diff --git a/api-sample/database/database-schema.sql b/api-sample/database/database-schema.sql index 0e1766bc..6549df5d 100644 --- a/api-sample/database/database-schema.sql +++ b/api-sample/database/database-schema.sql @@ -3,48 +3,121 @@ USE `{{PROJECT_NAME_SNAKECASE}}_db`; -- DONT MODIFY THIS MIGRATION -- it is used by Xest local development pipeline DROP TABLE IF EXISTS `migrations`; -CREATE TABLE `migrations` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `name` varchar(255) NOT NULL, - `run_on` datetime NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_unicode_ci; - -INSERT INTO `migrations` ( - name, - run_on -) VALUES ( - "/20211107064324-database-schema", - "20211107064324" -); + +CREATE TABLE + `migrations` ( + `id` int (11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `run_on` datetime NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE = InnoDB AUTO_INCREMENT = 0 DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_unicode_ci; + +INSERT INTO + `migrations` (name, run_on) +VALUES + ( + "/20211107064324-database-schema", + "20211107064324" + ); -- YOU CAN MODIFY BELOW THIS LINE -DROP TABLE IF EXISTS user_types; -CREATE TABLE user_types( - user_type_id int AUTO_INCREMENT PRIMARY KEY, - user_type VARCHAR(50) NOT NULL + +-- USER_ORGANIZATION_ROLES TABLE START +DROP TABLE IF EXISTS user_organization_roles; +CREATE TABLE user_organization_roles( + user_organization_role_id INT AUTO_INCREMENT PRIMARY KEY NOT NULL, + user_organization_role VARCHAR(100) NOT NULL ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_unicode_ci; +-- USER_ROLES TABLE END +-- USERS TABLE START DROP TABLE IF EXISTS users; -CREATE TABLE users( - user_id int AUTO_INCREMENT PRIMARY KEY, - first_name VARCHAR(50) NOT NULL, - last_name VARCHAR(50) NOT NULL, - email VARCHAR(50) NOT NULL UNIQUE, - password VARCHAR(500) NOT NULL, - user_type_id int NOT NULL, +CREATE TABLE + users ( + user_id int AUTO_INCREMENT PRIMARY KEY, + first_name VARCHAR(50) NOT NULL, + last_name VARCHAR(50) NOT NULL, + email VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(500) NOT NULL, + is_super_admin tinyint NOT NULL, + updated_at DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_unicode_ci; +-- USERS TABLE END + +-- ORGANIZATIONS TABLE START +DROP TABLE IF EXISTS organizations; +CREATE TABLE + organizations ( + organization_id INT AUTO_INCREMENT PRIMARY KEY NOT NULL, + organization_name VARCHAR(100) NOT NULL, + updated_at DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_unicode_ci; +-- ORGANIZATIONS TABLE END + +-- USER_ORGANIZATIONS TABLE START +DROP TABLE IF EXISTS user_organizations; +CREATE TABLE user_organizations( + user_organization_id INT AUTO_INCREMENT PRIMARY KEY NOT NULL, + user_id INT NOT NULL, + user_organization_role_id INT NOT NULL, + organization_id INT NOT NULL, + added_by INT NOT NULL, + updated_at DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_type_id) REFERENCES user_types(user_type_id) + FOREIGN KEY (user_id) REFERENCES users(user_id), + FOREIGN KEY (user_organization_role_id) REFERENCES user_organization_roles(user_organization_role_id), + FOREIGN KEY (organization_id) REFERENCES organizations(organization_id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_unicode_ci; +-- USER_ORGANIZATIONS TABLE END +-- USER_ORGANIZATION_INVITATIONS TABLE START +DROP TABLE IF EXISTS user_organization_invitations; +CREATE TABLE user_organization_invitations( + user_organization_invitation_id INT AUTO_INCREMENT PRIMARY KEY NOT NULL, + invitation_shortcode VARCHAR(36) NOT NULL, + organization_id INT NOT NULL, + email VARCHAR (200) NOT NULL, + invited_by INT NOT NULL, + user_organization_role_id INT NOT NULL, + comment VARCHAR(500), + sent_at DATETIME DEFAULT CURRENT_TIMESTAMP, + accepted_at DATETIME, + declined_at DATETIME, + updated_at DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_organization_role_id) REFERENCES user_organization_roles(user_organization_role_id), + FOREIGN KEY (organization_id) REFERENCES organizations(organization_id), + FOREIGN KEY (invited_by) REFERENCES user_organizations(user_id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_unicode_ci; +-- USER_ORGANIZATION_INVITATIONS TABLE END +-- USER_ORGANIZATION_REQUESTS TABLE START DROP TABLE IF EXISTS password_recovery_requests; CREATE TABLE password_recovery_requests( password_recovery_request_id int AUTO_INCREMENT PRIMARY KEY, requested_email VARCHAR(150) NOT NULL, - shortcode VARCHAR(40) NOT NULL UNIQUE, + shortcode VARCHAR(36) NOT NULL UNIQUE, recovered_at DATETIME, expiry_date DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (requested_email) REFERENCES users(email) -)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci; \ No newline at end of file +)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci; +-- USER_ORGANIZATION_REQUESTS TABLE END + +-- USER_ORGANIZATION_REQUESTS TABLE START +DROP TABLE IF EXISTS registration_requests; +CREATE TABLE registration_requests( + registration_request_id int AUTO_INCREMENT PRIMARY KEY, + first_name VARCHAR(50) NOT NULL, + last_name VARCHAR(50) NOT NULL, + email VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(500) NOT NULL, + is_super_admin int NOT NULL, + organization_name VARCHAR(50) NOT NULL, + registration_shortcode VARCHAR(36) NOT NULL UNIQUE, + status VARCHAR(20) DEFAULT 'pending', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_unicode_ci; +-- USER_ORGANIZATION_REQUESTS TABLE END \ No newline at end of file diff --git a/api-sample/database/seed-data.sql b/api-sample/database/seed-data.sql index 0e5ae202..613bc0c2 100644 --- a/api-sample/database/seed-data.sql +++ b/api-sample/database/seed-data.sql @@ -3,26 +3,180 @@ USE `{{PROJECT_NAME_SNAKECASE}}_db`; -- DONT MODIFY THIS MIGRATION -- it is used by Xest local development pipeline -INSERT INTO `migrations` ( - name, - run_on +INSERT INTO + `migrations` (name, run_on) +VALUES + ("/20211107064324-seed-data", "20211107064324"); + +-- YOU CAN MODIFY BELOW THIS LINE +INSERT INTO + user_organization_roles (user_organization_role_id, user_organization_role) +VALUES + (1, "OrganizationAdmin"), + (2, "Member"); + +INSERT INTO + users ( + user_id, + first_name, + last_name, + email, + password, + is_super_admin, + created_at + ) +VALUES + ( + 1, + "Ahmet", + "Akinsql", + "ahmet@akinsql.com", + SHA2 (CONCAT ("12345678", "SECRET_SALT"), 224), + 1, + "2020-11-20 12:00:00" + ), + ( + 2, + "Joe", + "Bloggs", + "joebloggs@gmail.com", + SHA2 (CONCAT ("12345678", "SECRET_SALT"), 224), + 0, + "2020-11-20 12:00:00" + ), + ( + 3, + "Jim", + "Bloggs", + "jimbloggs@yahoo.com", + SHA2 (CONCAT ("12345678", "SECRET_SALT"), 224), + 1, + "2020-11-20 12:00:00" + ), + ( + 4, + "Jane", + "Doe", + "janedoe@example.com", + SHA2 (CONCAT ("12345678", "SECRET_SALT"), 224), + 0, + "2020-11-20 12:00:00" + ); + +INSERT INTO + organizations (organization_name) +VALUES + ('Organization 1'), + ('Organization 2'), + ('Organization 3'), + ('Organization 4'), + ('Organization 5'); + +INSERT INTO + user_organizations ( + user_id, + user_organization_role_id, + organization_id, + added_by + ) +VALUES + (1, 1, 1, 1), + (2, 2, 2, 2), + (3, 1, 1, 3), + (4, 2, 2, 4); + +INSERT INTO + user_organization_invitations ( + invitation_shortcode, + organization_id, + email, + invited_by, + user_organization_role_id, + comment, + sent_at, + accepted_at + ) +VALUES + ( + 'shortcode1', + 1, + 'email1@example.com', + 1, + 1, + 'Comment 1', + '2022-01-01 00:00:00', + '2022-01-02 00:00:00' + ), + ( + 'shortcode2', + 1, + 'email2@example.com', + 1, + 2, + 'Comment 2', + '2022-01-02 00:00:00', + '2022-01-03 00:00:00' + ), + ( + 'shortcode3', + 2, + 'email3@example.com', + 2, + 1, + 'Comment 3', + '2022-01-03 00:00:00', + '2022-01-04 00:00:00' + ), + ( + 'shortcode4', + 2, + 'email4@example.com', + 2, + 2, + 'Comment 4', + '2022-01-04 00:00:00', + '2022-01-05 00:00:00' + ); + +-- INSERT INTO password_recovery_requests(shortcode,requested_email,expiry_date,created_at) +-- VALUES ("321","ahmet@akinsql.com","2020-09-20 12:30:00","2022-01-03 12:30:00"); + +INSERT INTO password_recovery_requests( + requested_email, + shortcode, + expiry_date ) VALUES ( - "/20211107064324-seed-data", - "20211107064324" + 'ahmet@akinsql.com', + 'shortcode1', + DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 1 DAY) +), ( + 'joebloggs@gmail.com', + 'shortcode2', + DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 1 DAY) ); --- YOU CAN MODIFY BELOW THIS LINE -INSERT INTO user_types (user_type_id, user_type) -VALUES (1, "admin"); -INSERT INTO user_types (user_type_id, user_type) -VALUES (2, "user"); - -INSERT INTO users (user_id, first_name, last_name, email, password, user_type_id, created_at) -VALUES (1, "Ahmet", "Akinsql", "ahmet@akinsql.com", SHA2(CONCAT("password","SECRET_SALT"), 224), 1, "2020-11-20 12:00:00"); -INSERT INTO users (user_id, first_name, last_name, email, password, user_type_id, created_at) -VALUES (2, "Joe", "Bloggs","joebloggs@gmail.com", SHA2(CONCAT("password","SECRET_SALT"), 224), 2, "2020-11-20 12:00:00"); -INSERT INTO users (user_id, first_name, last_name, email, password, user_type_id, created_at) -VALUES (3, "Jim", "Bloggs" , "jimbloggs@yahoo.com", SHA2(CONCAT("password","SECRET_SALT"), 224), 2, "2020-11-20 12:00:00"); - -INSERT INTO password_recovery_requests(shortcode,requested_email,expiry_date,created_at) -VALUES ("321","ahmet@akinsql.com","2020-09-20 12:30:00","2022-01-03 12:30:00"); \ No newline at end of file +INSERT INTO registration_requests( + first_name, + last_name, + email, + password, + is_super_admin, + organization_name, + registration_shortcode +) VALUES ( + 'John', + 'Doe', + 'john.doe@example.com', + SHA2 (CONCAT ("12345678", "SECRET_SALT"), 224), + 1, + 'Example Organization', + 'shortcode1' +), ( + 'Jane', + 'Doe', + 'joebloggs@gmail.com', + SHA2 (CONCAT ("12345678", "SECRET_SALT"), 224), + 0, + 'Example Organization 2', + 'shortcode2' +); \ No newline at end of file diff --git a/api-sample/package.json b/api-sample/package.json index 9e6184d6..391cb4d7 100644 --- a/api-sample/package.json +++ b/api-sample/package.json @@ -126,6 +126,7 @@ "jsonwebtoken": "^8.5.1", "jwks-rsa": "^1.5.0", "lodash": "^4.17.19", + "mailgun.js": "^10.1.0", "minio": "^7.0.19", "mjml": "^4.5.1", "module-alias": "^2.2.0", @@ -147,6 +148,6 @@ "winston-transport": "^4.3.0", "yamlify-object": "^0.5.1", "yamlify-object-colors": "^1.0.3", - "yup": "^0.27.0" + "yup": "^1.3.2" } } diff --git a/api-sample/src/actions/invitations/createOrganizationInvitation/index.js b/api-sample/src/actions/invitations/createOrganizationInvitation/index.js new file mode 100644 index 00000000..cd67e20d --- /dev/null +++ b/api-sample/src/actions/invitations/createOrganizationInvitation/index.js @@ -0,0 +1,23 @@ +const insertOrganizationInvitation = require("./queries/insertOrganizationInvitation"); + +const createOrganizationInvitation = async ({ + userId, + orgId, + email, + userOrganizationRoleId, + comment, + invitationShortcode +}) => { + const newInvitationId = await insertOrganizationInvitation({ + userId, + orgId, + email, + userOrganizationRoleId, + comment, + invitationShortcode + }); + + return { newInvitationId }; +}; + +module.exports = createOrganizationInvitation; diff --git a/api-sample/src/actions/invitations/createOrganizationInvitation/queries/insertOrganizationInvitation.js b/api-sample/src/actions/invitations/createOrganizationInvitation/queries/insertOrganizationInvitation.js new file mode 100644 index 00000000..6f4a2947 --- /dev/null +++ b/api-sample/src/actions/invitations/createOrganizationInvitation/queries/insertOrganizationInvitation.js @@ -0,0 +1,35 @@ +const { + submitQuery, + getInsertId, + sqlValueOrNull +} = require("~root/lib/database"); + +const insertOrganizationInvitation = ({ + userId, + orgId, + email, + userOrganizationRoleId, + comment, + invitationShortcode +}) => submitQuery` + INSERT INTO + user_organization_invitations( + email, + comment, + invitation_shortcode, + invited_by, + organization_id, + user_organization_role_id + ) +VALUES + ( + ${email}, + ${sqlValueOrNull(comment)}, + ${invitationShortcode}, + ${userId}, + ${orgId}, + ${userOrganizationRoleId} + ); +`; + +module.exports = getInsertId(insertOrganizationInvitation); diff --git a/api-sample/src/actions/invitations/fetchInvitationById/index.js b/api-sample/src/actions/invitations/fetchInvitationById/index.js new file mode 100644 index 00000000..473da273 --- /dev/null +++ b/api-sample/src/actions/invitations/fetchInvitationById/index.js @@ -0,0 +1,9 @@ +const selectInvitationById = require("./queries/selectInvitationById"); + +const fetchInvitationById = async ({ invitationId, shortCode }) => { + const invitation = await selectInvitationById({ invitationId, shortCode }); + + return { invitation }; +}; + +module.exports = fetchInvitationById; diff --git a/api-sample/src/actions/invitations/fetchInvitationById/queries/selectInvitationById.js b/api-sample/src/actions/invitations/fetchInvitationById/queries/selectInvitationById.js new file mode 100644 index 00000000..d04382f5 --- /dev/null +++ b/api-sample/src/actions/invitations/fetchInvitationById/queries/selectInvitationById.js @@ -0,0 +1,15 @@ +const { submitQuery, camelKeys, getFirst } = require("~root/lib/database"); + +const selectInvitationById = ({ invitationId, shortCode }) => submitQuery` + SELECT + user_organization_invitation_id, + organization_id, + invited_by, + user_role_id + FROM user_organization_invitations + WHERE user_organization_invitation_id = ${invitationId} + AND invitation_shortcode = ${shortCode} + AND accepted_at IS NULL +`; + +module.exports = getFirst(camelKeys(selectInvitationById)); diff --git a/api-sample/src/actions/invitations/fetchInvitationByShortCode/index.js b/api-sample/src/actions/invitations/fetchInvitationByShortCode/index.js new file mode 100644 index 00000000..db574c83 --- /dev/null +++ b/api-sample/src/actions/invitations/fetchInvitationByShortCode/index.js @@ -0,0 +1,9 @@ +const selectInvitationById = require("./queries/selectInvitationByShortCode"); + +const fetchInvitationByShortCode = async ({ shortCode }) => { + const invitation = await selectInvitationById({ shortCode }); + + return { invitation }; +}; + +module.exports = fetchInvitationByShortCode; diff --git a/api-sample/src/actions/invitations/fetchInvitationByShortCode/queries/selectInvitationByShortCode.js b/api-sample/src/actions/invitations/fetchInvitationByShortCode/queries/selectInvitationByShortCode.js new file mode 100644 index 00000000..d80ede34 --- /dev/null +++ b/api-sample/src/actions/invitations/fetchInvitationByShortCode/queries/selectInvitationByShortCode.js @@ -0,0 +1,15 @@ +const { submitQuery, camelKeys, getFirst } = require("~root/lib/database"); + +const selectInvitationByShortCode = ({ shortCode }) => submitQuery` + SELECT + user_organization_invitation_id, + organization_id, + invited_by, + user_organization_role_id, + email + FROM user_organization_invitations + WHERE invitation_shortcode = ${shortCode} + AND accepted_at IS NULL +`; + +module.exports = getFirst(camelKeys(selectInvitationByShortCode)); diff --git a/api-sample/src/actions/invitations/fetchUserInvitations/index.js b/api-sample/src/actions/invitations/fetchUserInvitations/index.js new file mode 100644 index 00000000..c846ba81 --- /dev/null +++ b/api-sample/src/actions/invitations/fetchUserInvitations/index.js @@ -0,0 +1,9 @@ +const selectUserInvitations = require("./queries/selectUserInvitations"); + +const fetchUserInvitations = async ({ email }) => { + const userInvitations = await selectUserInvitations({ email }); + + return { userInvitations }; +}; + +module.exports = fetchUserInvitations; diff --git a/api-sample/src/actions/invitations/fetchUserInvitations/queries/selectUserInvitations.js b/api-sample/src/actions/invitations/fetchUserInvitations/queries/selectUserInvitations.js new file mode 100644 index 00000000..8abb41d4 --- /dev/null +++ b/api-sample/src/actions/invitations/fetchUserInvitations/queries/selectUserInvitations.js @@ -0,0 +1,28 @@ +const { submitQuery, camelKeys } = require("~root/lib/database"); + +const selectUserInvitations = ({ email }) => submitQuery` + SELECT + user_organization_invitations.user_organization_invitation_id, + user_organization_invitations.organization_id, + user_organization_invitations.email, + user_organization_invitations.invited_by, + user_organization_invitations.accepted_at, + user_organization_invitations.user_type_id, + user_organization_invitations.sent_at, + user_organization_invitations.invitation_shortcode, + users.first_name AS invited_by_first_name, + users.last_name AS invited_by_last_name, + u2.first_name AS user_first_name, + u2.last_name AS user_last_name, + user_types.user_type, + organizations.organization_name + FROM user_organization_invitations + LEFT JOIN users ON users.user_id = user_organization_invitations.invited_by + LEFT JOIN users u2 ON u2.email = user_organization_invitations.email + LEFT JOIN user_types ON user_types.user_type_id = user_organization_invitations.user_type_id + LEFT JOIN organizations ON organizations.organization_id = user_organization_invitations.organization_id + WHERE user_organization_invitations.email = ${email} + ORDER BY user_organization_invitations.user_organization_invitation_id DESC +`; + +module.exports = camelKeys(selectUserInvitations); \ No newline at end of file diff --git a/api-sample/src/actions/invitations/modifyOrganizationInvitation/index.js b/api-sample/src/actions/invitations/modifyOrganizationInvitation/index.js new file mode 100644 index 00000000..5eb5c3a8 --- /dev/null +++ b/api-sample/src/actions/invitations/modifyOrganizationInvitation/index.js @@ -0,0 +1,17 @@ +const patchOrganizationInvitation = require("./queries/updateOrganizationInvitation"); + +const modifyOrganizationInvitation = async ({ + email, + invitationShortcode, + orgId +}) => { + const orgInvitation = await patchOrganizationInvitation({ + email, + invitationShortcode, + orgId + }); + + return { orgInvitation }; +}; + +module.exports = modifyOrganizationInvitation; diff --git a/api-sample/src/actions/invitations/modifyOrganizationInvitation/queries/updateOrganizationInvitation.js b/api-sample/src/actions/invitations/modifyOrganizationInvitation/queries/updateOrganizationInvitation.js new file mode 100644 index 00000000..edee038c --- /dev/null +++ b/api-sample/src/actions/invitations/modifyOrganizationInvitation/queries/updateOrganizationInvitation.js @@ -0,0 +1,26 @@ +const { + submitQuery, + sql, + sqlReduce, + getInsertId +} = require("~root/lib/database"); + +const updateOrganizationInvitation = ({ + email = null, + invitationShortcode, + orgId +}) => { + const updates = []; + if (email !== null) { + updates.push(sql`email = ${email}`); + } + + if (updates.length !== 0) { + return submitQuery`UPDATE user_organization_invitations SET ${updates.reduce( + sqlReduce + )} WHERE invitation_shortcode = ${invitationShortcode} && organization_id = ${orgId};`; + } + return Promise.resolve(); +}; + +module.exports = getInsertId(updateOrganizationInvitation); diff --git a/api-sample/src/actions/invitations/patchInvitationAsAccepted/index.js b/api-sample/src/actions/invitations/patchInvitationAsAccepted/index.js new file mode 100644 index 00000000..897ee0c5 --- /dev/null +++ b/api-sample/src/actions/invitations/patchInvitationAsAccepted/index.js @@ -0,0 +1,9 @@ +const acceptInvitationQuery = require("./queries/acceptInvitationQuery"); + +const patchInvitationAsAccepted = async ({ invitationId, shortCode }) => { + const invitation = await acceptInvitationQuery({ invitationId, shortCode }); + + return { invitation }; +}; + +module.exports = patchInvitationAsAccepted; diff --git a/api-sample/src/actions/invitations/patchInvitationAsAccepted/queries/acceptInvitationQuery.js b/api-sample/src/actions/invitations/patchInvitationAsAccepted/queries/acceptInvitationQuery.js new file mode 100644 index 00000000..0ee2f66d --- /dev/null +++ b/api-sample/src/actions/invitations/patchInvitationAsAccepted/queries/acceptInvitationQuery.js @@ -0,0 +1,19 @@ +const { submitQuery, sql, sqlReduce } = require("~root/lib/database"); + +const acceptInvitationQuery = ({ invitationId, shortCode }) => { + const updates = []; + + if (invitationId) { + updates.push(sql`accepted_at = CURRENT_TIMESTAMP`); + } + + if (updates.length !== 0) { + return submitQuery` + UPDATE + user_organization_invitations SET ${updates.reduce( + sqlReduce + )} WHERE invitation_shortcode = ${shortCode} && user_organization_invitation_id = ${invitationId}`; + } + return Promise.resolve(); +}; +module.exports = acceptInvitationQuery; diff --git a/api-sample/src/actions/invitations/removeInvitation/index.js b/api-sample/src/actions/invitations/removeInvitation/index.js new file mode 100644 index 00000000..62d89272 --- /dev/null +++ b/api-sample/src/actions/invitations/removeInvitation/index.js @@ -0,0 +1,10 @@ +const deleteInvitation = require("./queries/deleteInvitation"); + +const removeInvitation = async ({ invitationId }) => { + const invitation = await deleteInvitation({ + invitationId + }); + return { invitation }; +}; + +module.exports = removeInvitation; diff --git a/api-sample/src/actions/invitations/removeInvitation/queries/deleteInvitation.js b/api-sample/src/actions/invitations/removeInvitation/queries/deleteInvitation.js new file mode 100644 index 00000000..86d7d5cf --- /dev/null +++ b/api-sample/src/actions/invitations/removeInvitation/queries/deleteInvitation.js @@ -0,0 +1,8 @@ +const { submitQuery, getInsertId } = require("~root/lib/database"); + +const deleteInvitation = ({ invitationId }) => submitQuery` + DELETE + FROM user_organization_invitations + WHERE user_organization_invitation_id = ${invitationId} +`; +module.exports = getInsertId(deleteInvitation); diff --git a/api-sample/src/actions/organizations/createNewOrganization/index.js b/api-sample/src/actions/organizations/createNewOrganization/index.js new file mode 100644 index 00000000..54bd396e --- /dev/null +++ b/api-sample/src/actions/organizations/createNewOrganization/index.js @@ -0,0 +1,8 @@ +const insertOrganization = require("./queries/insertOrganization"); + +const createOrganization = async ({ organizationName }) => { + const organizationId = await insertOrganization({ organizationName }); + return { organizationId }; +}; + +module.exports = createOrganization; diff --git a/api-sample/src/actions/organizations/createNewOrganization/queries/insertOrganization.js b/api-sample/src/actions/organizations/createNewOrganization/queries/insertOrganization.js new file mode 100644 index 00000000..6ece38ce --- /dev/null +++ b/api-sample/src/actions/organizations/createNewOrganization/queries/insertOrganization.js @@ -0,0 +1,8 @@ +const { submitQuery, getInsertId } = require("~root/lib/database"); + +const insertOrganization = ({ organizationName }) => submitQuery` + INSERT INTO organizations(organization_name) + VALUES(${organizationName}); +`; + +module.exports = getInsertId(insertOrganization); diff --git a/api-sample/src/actions/organizations/createOrganization/index.js b/api-sample/src/actions/organizations/createOrganization/index.js new file mode 100644 index 00000000..cd4cafa3 --- /dev/null +++ b/api-sample/src/actions/organizations/createOrganization/index.js @@ -0,0 +1,9 @@ +const insertOrganization = require("./queries/insertOrganization"); + +const createOrganization = async ({ organizationName }) => { + const organizationId = await insertOrganization({ organizationName }); + + return { organizationId }; +}; + +module.exports = createOrganization; diff --git a/api-sample/src/actions/organizations/createOrganization/queries/insertOrganization.js b/api-sample/src/actions/organizations/createOrganization/queries/insertOrganization.js new file mode 100644 index 00000000..dce4a1be --- /dev/null +++ b/api-sample/src/actions/organizations/createOrganization/queries/insertOrganization.js @@ -0,0 +1,15 @@ +const { submitQuery, getInsertId } = require("~root/lib/database"); + +const insertOrganization = ({ organizationName }) => submitQuery` + INSERT INTO + organizations + ( + organization_name + ) + VALUES + ( + ${organizationName} + ) +`; + +module.exports = getInsertId(insertOrganization); diff --git a/api-sample/src/actions/organizations/createOrganizationUser/index.js b/api-sample/src/actions/organizations/createOrganizationUser/index.js new file mode 100644 index 00000000..e63bbbd0 --- /dev/null +++ b/api-sample/src/actions/organizations/createOrganizationUser/index.js @@ -0,0 +1,19 @@ +const insertOrganizationUser = require("./queries/insertOrganizationUser"); + +const createOrganizationUser = async ({ + userId, + userOrganizationRoleId, + organizationId, + addedBy +}) => { + const organizationUserId = await insertOrganizationUser({ + userId, + userOrganizationRoleId, + organizationId, + addedBy + }); + + return { organizationUserId }; +}; + +module.exports = createOrganizationUser; diff --git a/api-sample/src/actions/organizations/createOrganizationUser/queries/insertOrganizationUser.js b/api-sample/src/actions/organizations/createOrganizationUser/queries/insertOrganizationUser.js new file mode 100644 index 00000000..ce42b264 --- /dev/null +++ b/api-sample/src/actions/organizations/createOrganizationUser/queries/insertOrganizationUser.js @@ -0,0 +1,25 @@ +const { submitQuery, getInsertId } = require("~root/lib/database"); + +const insertOrganizationUser = ({ + userId, + userOrganizationRoleId, + organizationId, + addedBy +}) => submitQuery` + INSERT INTO user_organizations + ( + user_id, + user_organization_role_id, + organization_id, + added_by + ) + VALUES + ( + ${userId}, + ${userOrganizationRoleId}, + ${organizationId}, + ${addedBy} + ) +`; + +module.exports = getInsertId(insertOrganizationUser); diff --git a/api-sample/src/actions/organizations/fetchOrganizationInvitations/index.js b/api-sample/src/actions/organizations/fetchOrganizationInvitations/index.js new file mode 100644 index 00000000..f99c611a --- /dev/null +++ b/api-sample/src/actions/organizations/fetchOrganizationInvitations/index.js @@ -0,0 +1,9 @@ +const selectOrganizationInvitations = require("./queries/selectOrganizationInvitations"); + +const fetchOrganizationInvitations = async ({ orgId }) => { + const invitations = await selectOrganizationInvitations({ orgId }); + + return { invitations }; +}; + +module.exports = fetchOrganizationInvitations; diff --git a/api-sample/src/actions/organizations/fetchOrganizationInvitations/queries/selectOrganizationInvitations.js b/api-sample/src/actions/organizations/fetchOrganizationInvitations/queries/selectOrganizationInvitations.js new file mode 100644 index 00000000..74048255 --- /dev/null +++ b/api-sample/src/actions/organizations/fetchOrganizationInvitations/queries/selectOrganizationInvitations.js @@ -0,0 +1,24 @@ +const { submitQuery, camelKeys } = require("~root/lib/database"); + +const selectOrganizationInvitations = ({ orgId }) => submitQuery` + SELECT + user_organization_invitations.user_organization_invitation_id, + user_organization_invitations.invitation_shortcode, + user_organization_invitations.organization_id, + user_organization_invitations.email, + user_organization_invitations.invited_by, + user_organization_invitations.user_organization_role_id, + user_organization_invitations.comment, + user_organization_invitations.sent_at, + user_organization_invitations.accepted_at, + user_organization_invitations.declined_at, + user_organization_invitations.updated_at, + users.first_name as invitedByFirstName, + users.last_name as invitedByLastName + FROM user_organization_invitations + LEFT JOIN users ON users.user_id = user_organization_invitations.invited_by + WHERE organization_id = ${orgId} + ORDER BY sent_at DESC +`; + +module.exports = camelKeys(selectOrganizationInvitations); diff --git a/api-sample/src/actions/organizations/fetchOrganizationNameById/index.js b/api-sample/src/actions/organizations/fetchOrganizationNameById/index.js new file mode 100644 index 00000000..ff5a5073 --- /dev/null +++ b/api-sample/src/actions/organizations/fetchOrganizationNameById/index.js @@ -0,0 +1,9 @@ +const selectOrganizationNameById = require("./queries/selectOrganizationNameById"); + +const fetchOrganizationNameById = async ({ organizationId }) => { + const organizationName = await selectOrganizationNameById({ organizationId }); + + return { organizationName }; +}; + +module.exports = fetchOrganizationNameById; diff --git a/api-sample/src/actions/organizations/fetchOrganizationNameById/queries/selectOrganizationNameById.js b/api-sample/src/actions/organizations/fetchOrganizationNameById/queries/selectOrganizationNameById.js new file mode 100644 index 00000000..213519ed --- /dev/null +++ b/api-sample/src/actions/organizations/fetchOrganizationNameById/queries/selectOrganizationNameById.js @@ -0,0 +1,10 @@ +const { submitQuery, camelKeys } = require("~root/lib/database"); + +const selectOrganizationNameById = ({ organizationId }) => submitQuery` + SELECT + organization_name + FROM organizations + WHERE organization_id = ${organizationId} +`; + +module.exports = camelKeys(selectOrganizationNameById); diff --git a/api-sample/src/actions/organizations/fetchOrganizationUsers/index.js b/api-sample/src/actions/organizations/fetchOrganizationUsers/index.js new file mode 100644 index 00000000..d36a4090 --- /dev/null +++ b/api-sample/src/actions/organizations/fetchOrganizationUsers/index.js @@ -0,0 +1,9 @@ +const selectOrganizationUsers = require("./queries/selectOrganizationUsers"); + +const fetchOrganizationUsers = async ({ orgId }) => { + const organizationUsers = await selectOrganizationUsers({ orgId }); + + return { organizationUsers }; +}; + +module.exports = fetchOrganizationUsers; diff --git a/api-sample/src/actions/organizations/fetchOrganizationUsers/queries/selectOrganizationUsers.js b/api-sample/src/actions/organizations/fetchOrganizationUsers/queries/selectOrganizationUsers.js new file mode 100644 index 00000000..a08de287 --- /dev/null +++ b/api-sample/src/actions/organizations/fetchOrganizationUsers/queries/selectOrganizationUsers.js @@ -0,0 +1,21 @@ +const { submitQuery, camelKeys } = require("~root/lib/database"); + +const selectOrganizationUsers = ({ orgId }) => submitQuery` + SELECT + user_organizations.user_organization_id, + user_organizations.user_id, + user_organizations.user_organization_role_id, + user_organizations.organization_id, + users.first_name, + users.last_name, + users.email, + user_organization_roles.user_organization_role, + user_organizations.updated_at, + user_organizations.created_at + FROM user_organizations + LEFT JOIN users ON users.user_id = user_organizations.user_id + LEFT JOIN user_organization_roles ON user_organizations.user_organization_role_id = user_organizations.user_organization_role_id + WHERE organization_id = ${orgId} +`; + +module.exports = camelKeys(selectOrganizationUsers); diff --git a/api-sample/src/actions/organizations/fetchOrganizationsByEmail/index.js b/api-sample/src/actions/organizations/fetchOrganizationsByEmail/index.js new file mode 100644 index 00000000..a9c63237 --- /dev/null +++ b/api-sample/src/actions/organizations/fetchOrganizationsByEmail/index.js @@ -0,0 +1,9 @@ +const selectOrgOverview = require("./queries/selectOrganizationByEmail"); + +const fetchOrganizationsByEmail = async ({ email }) => { + const organizations = await selectOrgOverview({ email }); + + return { organizations }; +}; + +module.exports = fetchOrganizationsByEmail; diff --git a/api-sample/src/actions/organizations/fetchOrganizationsByEmail/queries/selectOrganizationByEmail.js b/api-sample/src/actions/organizations/fetchOrganizationsByEmail/queries/selectOrganizationByEmail.js new file mode 100644 index 00000000..b11feccf --- /dev/null +++ b/api-sample/src/actions/organizations/fetchOrganizationsByEmail/queries/selectOrganizationByEmail.js @@ -0,0 +1,16 @@ +const { submitQuery, camelKeys } = require("~root/lib/database"); + +const selectOrganizationByEmail = ({ email }) => submitQuery` + SELECT + organizations.organization_id, + organizations.organization_name, + user_organization_roles.user_organization_role, + user_organization_roles.user_organization_role_id + FROM user_organizations + LEFT JOIN users ON user_organizations.user_id = users.user_id + LEFT JOIN organizations ON user_organizations.organization_id = organizations.organization_id + LEFT JOIN user_organization_roles ON user_organizations.user_organization_role_id = user_organization_roles.user_organization_role_id + WHERE users.email = ${email} +`; + +module.exports = camelKeys(selectOrganizationByEmail); diff --git a/api-sample/src/actions/organizations/fetchOrganizationsByUserId/index.js b/api-sample/src/actions/organizations/fetchOrganizationsByUserId/index.js new file mode 100644 index 00000000..d2a7e2f3 --- /dev/null +++ b/api-sample/src/actions/organizations/fetchOrganizationsByUserId/index.js @@ -0,0 +1,9 @@ +const selectOrganizationsByUserId = require("./queries/selectOrganizationsByUserId"); + +const fetchOrganizationsByUserId = async ({ userId }) => { + const organizations = await selectOrganizationsByUserId({ userId }); + + return { organizations }; +}; + +module.exports = fetchOrganizationsByUserId; diff --git a/api-sample/src/actions/organizations/fetchOrganizationsByUserId/queries/selectOrganizationsByUserId.js b/api-sample/src/actions/organizations/fetchOrganizationsByUserId/queries/selectOrganizationsByUserId.js new file mode 100644 index 00000000..c81478ad --- /dev/null +++ b/api-sample/src/actions/organizations/fetchOrganizationsByUserId/queries/selectOrganizationsByUserId.js @@ -0,0 +1,16 @@ +const { submitQuery, camelKeys } = require("~root/lib/database"); + +const selectOrganizationsByUserId = ({ userId }) => submitQuery` + SELECT + organizations.organization_id, + organizations.organization_name, + user_organization_roles.user_organization_role, + user_organization_roles.user_organization_role_id + FROM user_organizations + LEFT JOIN users ON user_organizations.user_id = users.user_id + LEFT JOIN organizations ON user_organizations.organization_id = organizations.organization_id + LEFT JOIN user_organization_roles ON user_organizations.user_organization_role_id = user_organization_roles.user_organization_role_id + WHERE users.user_id = ${userId} +`; + +module.exports = camelKeys(selectOrganizationsByUserId); diff --git a/api-sample/src/actions/organizations/modifyOrganizationUser/index.js b/api-sample/src/actions/organizations/modifyOrganizationUser/index.js new file mode 100644 index 00000000..927f448b --- /dev/null +++ b/api-sample/src/actions/organizations/modifyOrganizationUser/index.js @@ -0,0 +1,21 @@ +const updateOrganizationUser = require("./queries/updateOrganizationUser"); + +const modifyOrganizationUser = async ({ + userId, + orgId, + orgUserId, + userRoleId, + enabled +}) => { + const updatedorgUserId = await updateOrganizationUser({ + userId, + orgId, + orgUserId, + userRoleId, + enabled + }); + + return { updatedorgUserId }; +}; + +module.exports = modifyOrganizationUser; diff --git a/api-sample/src/actions/organizations/modifyOrganizationUser/queries/updateOrganizationUser.js b/api-sample/src/actions/organizations/modifyOrganizationUser/queries/updateOrganizationUser.js new file mode 100644 index 00000000..c1ce506f --- /dev/null +++ b/api-sample/src/actions/organizations/modifyOrganizationUser/queries/updateOrganizationUser.js @@ -0,0 +1,41 @@ +const { submitQuery, sql, sqlReduce } = require("~root/lib/database"); + +const updateOrganizationUser = ({ + userId, + orgId, + orgUserId, + userRoleId = null, + enabled = null +}) => { + const updates = []; + + if (userRoleId !== null) { + updates.push(sql`user_role_id = ${userRoleId}`); + } + + if (enabled !== null) { + if (enabled === true) { + updates.push(sql`disabled_by = null`); + updates.push(sql`disabled_at = null`); + } + if (enabled === false) { + updates.push(sql`disabled_by = ${userId}`); + updates.push(sql`disabled_at = CURRENT_TIMESTAMP`); + } + } + + if (updates.length !== 0) { + updates.push(sql`updated_at = CURRENT_TIMESTAMP`); + return submitQuery` + UPDATE + user_organizations + SET + ${updates.reduce(sqlReduce)} + WHERE + user_id = ${orgUserId} && organization_id = ${orgId} + `; + } + return Promise.resolve(); +}; + +module.exports = updateOrganizationUser; diff --git a/api-sample/src/actions/organizations/removeOrganizationUser/index.js b/api-sample/src/actions/organizations/removeOrganizationUser/index.js new file mode 100644 index 00000000..e5dd4f4a --- /dev/null +++ b/api-sample/src/actions/organizations/removeOrganizationUser/index.js @@ -0,0 +1,11 @@ +const deleteOrganizationUser = require("./queries/deleteOrganizationUser"); + +const removeOrganizationUser = async ({ orgUserId, orgId }) => { + const deletedOrgUser = await deleteOrganizationUser({ + orgUserId, + orgId + }); + return { deletedOrgUser }; +}; + +module.exports = removeOrganizationUser; diff --git a/api-sample/src/actions/organizations/removeOrganizationUser/queries/deleteOrganizationUser.js b/api-sample/src/actions/organizations/removeOrganizationUser/queries/deleteOrganizationUser.js new file mode 100644 index 00000000..d31dea92 --- /dev/null +++ b/api-sample/src/actions/organizations/removeOrganizationUser/queries/deleteOrganizationUser.js @@ -0,0 +1,9 @@ +const { submitQuery } = require("~root/lib/database"); + +const deleteOrganizationUser = ({ orgUserId, orgId }) => + submitQuery` + DELETE FROM user_organizations + WHERE user_id = ${orgUserId} && organization_id = ${orgId}; + `; + +module.exports = deleteOrganizationUser; diff --git a/api-sample/src/actions/password-recovery/createRecoveryRequest/queries/insertRecoveryRequest.js b/api-sample/src/actions/password-recovery/createRecoveryRequest/queries/insertRecoveryRequest.js index 24067627..0215de9c 100644 --- a/api-sample/src/actions/password-recovery/createRecoveryRequest/queries/insertRecoveryRequest.js +++ b/api-sample/src/actions/password-recovery/createRecoveryRequest/queries/insertRecoveryRequest.js @@ -1,8 +1,8 @@ const { submitQuery, getInsertId } = require("~root/lib/database"); const insertRecoveryRequest = ({ requestedEmail, URLshortcode }) => submitQuery` - INSERT INTO - password_recovery_requests ( + INSERT INTO + password_recovery_requests ( requested_email, shortcode, expiry_date diff --git a/api-sample/src/actions/password-recovery/modifyPassword/queries/updatePassword.js b/api-sample/src/actions/password-recovery/modifyPassword/queries/updatePassword.js index 64bf600e..317b1e54 100644 --- a/api-sample/src/actions/password-recovery/modifyPassword/queries/updatePassword.js +++ b/api-sample/src/actions/password-recovery/modifyPassword/queries/updatePassword.js @@ -1,9 +1,9 @@ const { submitQuery, getInsertId } = require("~root/lib/database"); const updatePassword = ({ email, password }) => submitQuery` - -UPDATE users -SET password = SHA2(CONCAT(${password},${process.env.PASSWORD_SALT}), 224) WHERE email= ${email}; + UPDATE users + SET password = SHA2(CONCAT(${password},${process.env.PASSWORD_SALT}), 224) + WHERE email= ${email}; `; module.exports = getInsertId(updatePassword); diff --git a/api-sample/src/actions/password-recovery/modifyRecoveryDate/queries/updateRecoveryDate.js b/api-sample/src/actions/password-recovery/modifyRecoveryDate/queries/updateRecoveryDate.js index 83268bf1..1cb280d7 100644 --- a/api-sample/src/actions/password-recovery/modifyRecoveryDate/queries/updateRecoveryDate.js +++ b/api-sample/src/actions/password-recovery/modifyRecoveryDate/queries/updateRecoveryDate.js @@ -1,11 +1,9 @@ const { submitQuery, getInsertId } = require("~root/lib/database"); const updateRecoveryDate = ({ email }) => submitQuery` - UPDATE password_recovery_requests SET recovered_at = CURRENT_TIMESTAMP WHERE requested_email = ${email}; - - `; +`; module.exports = getInsertId(updateRecoveryDate); diff --git a/api-sample/src/actions/schemaHelpers/queries/selectEventUser/selectEventUser.js b/api-sample/src/actions/schemaHelpers/queries/selectEventUser/selectEventUser.js new file mode 100644 index 00000000..18814c4b --- /dev/null +++ b/api-sample/src/actions/schemaHelpers/queries/selectEventUser/selectEventUser.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectEventUser = ({ orgId, userId }) => submitQuery` + SELECT + user_organization_id + FROM user_organizations + WHERE user_id = ${userId} && organization_id = ${orgId} && (user_role_id = 3 || user_role_id = 4 ) +`; + +module.exports = getFirst(selectEventUser, "user_organization_id"); diff --git a/api-sample/src/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser.js b/api-sample/src/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser.js new file mode 100644 index 00000000..0f37a0f8 --- /dev/null +++ b/api-sample/src/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectOrganizationUser = ({ orgId, userId }) => submitQuery` + SELECT + user_organization_id + FROM user_organizations + WHERE user_id = ${userId} && organization_id = ${orgId} +`; + +module.exports = getFirst(selectOrganizationUser, "user_organization_id"); diff --git a/api-sample/src/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin.js b/api-sample/src/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin.js new file mode 100644 index 00000000..2bdd688a --- /dev/null +++ b/api-sample/src/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectSuperAdmin = ({ userId }) => submitQuery` + SELECT + user_id + FROM users + WHERE user_id = ${userId} && is_super_admin = 1; +`; + +module.exports = getFirst(selectSuperAdmin, "user_id"); diff --git a/api-sample/src/actions/schemaHelpers/queries/selectUserById/selectUserById.js b/api-sample/src/actions/schemaHelpers/queries/selectUserById/selectUserById.js new file mode 100644 index 00000000..e155c461 --- /dev/null +++ b/api-sample/src/actions/schemaHelpers/queries/selectUserById/selectUserById.js @@ -0,0 +1,11 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserById = ({ userId }) => submitQuery` + SELECT + first_name, + email + FROM users + WHERE user_id = ${userId} +`; + +module.exports = getFirst(selectUserById); diff --git a/api-sample/src/actions/schemaHelpers/queries/selectUserById/selectUserByOrgId.js b/api-sample/src/actions/schemaHelpers/queries/selectUserById/selectUserByOrgId.js new file mode 100644 index 00000000..bcedfc21 --- /dev/null +++ b/api-sample/src/actions/schemaHelpers/queries/selectUserById/selectUserByOrgId.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserByOrgId = ({ userId, orgId }) => submitQuery` + SELECT + user_organization_id + FROM user_organizations + WHERE user_id = ${userId} && organization_id = ${orgId} +`; + +module.exports = getFirst(selectUserByOrgId); diff --git a/api-sample/src/actions/users/createCompleteRegistration/index.js b/api-sample/src/actions/users/createCompleteRegistration/index.js new file mode 100644 index 00000000..ffc24e52 --- /dev/null +++ b/api-sample/src/actions/users/createCompleteRegistration/index.js @@ -0,0 +1,21 @@ +const insertCompleteRegistration = require("./queries/insertCompleteRegistration"); + +const createCompleteRegistration = async ({ + firstName, + lastName, + email, + password, + companyName +}) => { + const userId = await insertCompleteRegistration({ + firstName, + lastName, + email, + password, + companyName + }); + + return { userId }; +}; + +module.exports = createCompleteRegistration; diff --git a/api-sample/src/actions/users/createCompleteRegistration/queries/insertCompleteRegistration.js b/api-sample/src/actions/users/createCompleteRegistration/queries/insertCompleteRegistration.js new file mode 100644 index 00000000..5cda41cc --- /dev/null +++ b/api-sample/src/actions/users/createCompleteRegistration/queries/insertCompleteRegistration.js @@ -0,0 +1,21 @@ +const { submitQuery, getInsertId } = require("~root/lib/database"); + +const insertUser = ({ firstName, lastName, email, password }) => submitQuery` + INSERT INTO users ( + first_name, + last_name, + email, + password, + is_super_admin + ) + VALUES + ( + ${firstName}, + ${lastName}, + ${email}, + ${password}, + 0 + ); +`; + +module.exports = getInsertId(insertUser); diff --git a/api-sample/src/actions/users/createOrganizationInvitation/index.js b/api-sample/src/actions/users/createOrganizationInvitation/index.js new file mode 100644 index 00000000..50cf6023 --- /dev/null +++ b/api-sample/src/actions/users/createOrganizationInvitation/index.js @@ -0,0 +1,23 @@ +const insertOrganizationInvitation = require("./queries/insertInvitation"); + +const createOrganizationInvitation = async ({ + userId, + orgId, + email, + userOrganizationRoleId, + comment, + invitationShortcode +}) => { + const newInvitationId = await insertOrganizationInvitation({ + userId, + orgId, + email, + userOrganizationRoleId, + comment, + invitationShortcode + }); + + return { newInvitationId }; +}; + +module.exports = createOrganizationInvitation; diff --git a/api-sample/src/actions/users/createOrganizationInvitation/queries/insertInvitation.js b/api-sample/src/actions/users/createOrganizationInvitation/queries/insertInvitation.js new file mode 100644 index 00000000..a24b1783 --- /dev/null +++ b/api-sample/src/actions/users/createOrganizationInvitation/queries/insertInvitation.js @@ -0,0 +1,35 @@ +const { + submitQuery, + getInsertId, + sqlValueOrNull +} = require("~root/lib/database"); + +const insertOrganizationInvitation = ({ + userId, + orgId, + email, + userOrganizationRoleId, + comment, + invitationShortcode +}) => submitQuery` + INSERT INTO + user_organization_invitations( + email, + comment, + invitation_shortcode, + invited_by, + organization_id, + user_organization_role_id + ) +VALUES + ( + ${email}, + ${sqlValueOrNull(comment)}, + ${invitationShortcode}, + ${userId}, + ${orgId}, + ${userOrganizationRoleId} + ); +`; + +module.exports = getInsertId(insertOrganizationInvitation); diff --git a/api-sample/src/actions/users/createRegistrationRequest/index.js b/api-sample/src/actions/users/createRegistrationRequest/index.js new file mode 100644 index 00000000..04d59328 --- /dev/null +++ b/api-sample/src/actions/users/createRegistrationRequest/index.js @@ -0,0 +1,23 @@ +const insertRegistrationRequest = require("./queries/insertRegistrationRequest"); + +const createRegistrationRequest = async ({ + firstName, + lastName, + email, + password, + organizationName, + registrationShortcode +}) => { + const createdRegistrationRequest = await insertRegistrationRequest({ + firstName, + lastName, + email, + password, + organizationName, + registrationShortcode + }); + + return { createdRegistrationRequest }; +}; + +module.exports = createRegistrationRequest; diff --git a/api-sample/src/actions/users/createRegistrationRequest/queries/insertRegistrationRequest.js b/api-sample/src/actions/users/createRegistrationRequest/queries/insertRegistrationRequest.js new file mode 100644 index 00000000..4fde7845 --- /dev/null +++ b/api-sample/src/actions/users/createRegistrationRequest/queries/insertRegistrationRequest.js @@ -0,0 +1,34 @@ +const { submitQuery, getInsertId } = require("~root/lib/database"); + +const insertUser = ({ + firstName, + lastName, + email, + password, + organizationName, + registrationShortcode +}) => submitQuery` + INSERT INTO registration_requests ( + first_name, + last_name, + email, + password, + is_super_admin, + organization_name, + registration_shortcode, + status + ) + VALUES + ( + ${firstName}, + ${lastName}, + ${email}, + SHA2(CONCAT(${password},${process.env.PASSWORD_SALT}), 224), + 0, + ${organizationName}, + ${registrationShortcode}, + "pending" + ); +`; + +module.exports = getInsertId(insertUser); diff --git a/api-sample/src/actions/users/createUser/index.js b/api-sample/src/actions/users/createUser/index.js index a06f14ed..367bafba 100644 --- a/api-sample/src/actions/users/createUser/index.js +++ b/api-sample/src/actions/users/createUser/index.js @@ -5,14 +5,14 @@ const createUser = async ({ lastName, email, password, - userTypeId + isSuperAdmin }) => { const user = await insertUser({ firstName, lastName, email, password, - userTypeId + isSuperAdmin }); return { user }; diff --git a/api-sample/src/actions/users/createUser/queries/insertUser.js b/api-sample/src/actions/users/createUser/queries/insertUser.js index 6087b56d..6ce354d6 100644 --- a/api-sample/src/actions/users/createUser/queries/insertUser.js +++ b/api-sample/src/actions/users/createUser/queries/insertUser.js @@ -5,7 +5,7 @@ const insertUser = ({ lastName, email, password, - userTypeId + isSuperAdmin }) => submitQuery` INSERT INTO users ( @@ -13,7 +13,7 @@ const insertUser = ({ last_name, email, password, - user_type_id + is_super_admin ) VALUES ( @@ -21,7 +21,7 @@ const insertUser = ({ ${lastName}, ${email}, SHA2(CONCAT(${password},${process.env.PASSWORD_SALT}), 224), - ${userTypeId} + ${isSuperAdmin} ) `; diff --git a/api-sample/src/actions/users/fetchRegistrationRequestByShortcode/index.js b/api-sample/src/actions/users/fetchRegistrationRequestByShortcode/index.js new file mode 100644 index 00000000..c74ad96b --- /dev/null +++ b/api-sample/src/actions/users/fetchRegistrationRequestByShortcode/index.js @@ -0,0 +1,13 @@ +const selectRegistrationRequestByShortcode = require("./queries/selectRegistrationRequestByShortcode"); + +const fetchRegistrationRequestByShortcode = async ({ + registrationShortcode +}) => { + const potentialNewUserData = await selectRegistrationRequestByShortcode({ + registrationShortcode + }); + + return { potentialNewUserData }; +}; + +module.exports = fetchRegistrationRequestByShortcode; diff --git a/api-sample/src/actions/users/fetchRegistrationRequestByShortcode/queries/selectRegistrationRequestByShortcode.js b/api-sample/src/actions/users/fetchRegistrationRequestByShortcode/queries/selectRegistrationRequestByShortcode.js new file mode 100644 index 00000000..1a3265d6 --- /dev/null +++ b/api-sample/src/actions/users/fetchRegistrationRequestByShortcode/queries/selectRegistrationRequestByShortcode.js @@ -0,0 +1,18 @@ +const { submitQuery, camelKeys, getFirst } = require("~root/lib/database"); + +const selectRegistrationRequestByShortcode = ({ + registrationShortcode +}) => submitQuery` + SELECT + first_name, + last_name, + email, + password, + organization_name + FROM registration_requests + WHERE registration_shortcode = ${registrationShortcode} + ORDER BY created_at DESC + LIMIT 1 +`; + +module.exports = getFirst(camelKeys(selectRegistrationRequestByShortcode)); diff --git a/api-sample/src/actions/users/fetchRequestByShortcode/index.js b/api-sample/src/actions/users/fetchRequestByShortcode/index.js new file mode 100644 index 00000000..d0eedc53 --- /dev/null +++ b/api-sample/src/actions/users/fetchRequestByShortcode/index.js @@ -0,0 +1,9 @@ +const selectRequestByShortcode = require("./queries/selectRequestByShortcode"); + +const fetchRequestByShortcode = async ({ shortcode }) => { + const request = await selectRequestByShortcode({ shortcode }); + + return { request }; +}; + +module.exports = fetchRequestByShortcode; diff --git a/api-sample/src/actions/users/fetchRequestByShortcode/queries/selectRequestByShortcode.js b/api-sample/src/actions/users/fetchRequestByShortcode/queries/selectRequestByShortcode.js new file mode 100644 index 00000000..93b0a742 --- /dev/null +++ b/api-sample/src/actions/users/fetchRequestByShortcode/queries/selectRequestByShortcode.js @@ -0,0 +1,15 @@ +const { submitQuery, camelKeys, getFirst } = require("~root/lib/database"); + +const selectRequestByShortcode = ({ shortcode }) => submitQuery` + SELECT + password_recovery_request_id, + email, + shortcode, + recovered_at, + expiry_date, + created_at + FROM password_recovery_requests + WHERE shortcode = ${shortcode} +`; + +module.exports = getFirst(camelKeys(selectRequestByShortcode)); diff --git a/api-sample/src/actions/users/fetchUser/queries/selectUser.js b/api-sample/src/actions/users/fetchUser/queries/selectUser.js index d5b5e4a6..6c7a3deb 100644 --- a/api-sample/src/actions/users/fetchUser/queries/selectUser.js +++ b/api-sample/src/actions/users/fetchUser/queries/selectUser.js @@ -7,10 +7,10 @@ const selectUser = ({ email, password }) => submitQuery` last_name, password, email, - user_types.user_type_id, - user_types.user_type + user_roles.user_role_id, + user_roles.user_role FROM users - LEFT JOIN user_types ON users.user_type_id = user_types.user_type_id + LEFT JOIN user_roles ON users.user_role_id = user_roles.user_role_id WHERE email = ${email} AND password = SHA2(CONCAT(${password}, ${process.env.PASSWORD_SALT}), 224); `; diff --git a/api-sample/src/actions/users/fetchUserByEmail/index.js b/api-sample/src/actions/users/fetchUserByEmail/index.js new file mode 100644 index 00000000..a5363f55 --- /dev/null +++ b/api-sample/src/actions/users/fetchUserByEmail/index.js @@ -0,0 +1,11 @@ +const selectUserIdByEmail = require("./queries/selectUserIdByEmail"); + +const fetchUserIdByEmail = async ({ email }) => { + const newOrgUserUserId = await selectUserIdByEmail({ + email + }); + + return { newOrgUserUserId }; +}; + +module.exports = fetchUserIdByEmail; diff --git a/api-sample/src/actions/users/fetchUserByEmail/queries/selectUserIdByEmail.js b/api-sample/src/actions/users/fetchUserByEmail/queries/selectUserIdByEmail.js new file mode 100644 index 00000000..116de09d --- /dev/null +++ b/api-sample/src/actions/users/fetchUserByEmail/queries/selectUserIdByEmail.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserIdByEmail = ({ email }) => submitQuery` + SELECT + user_id + FROM users + WHERE email = ${email} +`; + +module.exports = getFirst(selectUserIdByEmail, "user_id"); diff --git a/api-sample/src/actions/users/fetchUserById/queries/selectUserById.js b/api-sample/src/actions/users/fetchUserById/queries/selectUserById.js index 58e4e154..4bede5a8 100644 --- a/api-sample/src/actions/users/fetchUserById/queries/selectUserById.js +++ b/api-sample/src/actions/users/fetchUserById/queries/selectUserById.js @@ -1,14 +1,12 @@ const { submitQuery, camelKeys, getFirst } = require("~root/lib/database"); const selectUserById = ({ userId }) => submitQuery` - SELECT - user_id, + SELECT first_name, last_name, - user_types.user_type_id, - user_types.user_type + email, + is_super_admin, FROM users - LEFT JOIN user_types ON users.user_type_id = user_types.user_type_id WHERE user_id = ${userId} `; diff --git a/api-sample/src/actions/users/fetchUserInvitation/index.js b/api-sample/src/actions/users/fetchUserInvitation/index.js new file mode 100644 index 00000000..8e21601b --- /dev/null +++ b/api-sample/src/actions/users/fetchUserInvitation/index.js @@ -0,0 +1,13 @@ +const selectUserInvitation = require("./queries/selectUserInvitation"); + +const fetchUserInvitation = async ({ email, invitationId, shortcode }) => { + const invitation = await selectUserInvitation({ + email, + invitationId, + shortcode + }); + + return { invitation }; +}; + +module.exports = fetchUserInvitation; diff --git a/api-sample/src/actions/users/fetchUserInvitation/queries/selectUserInvitation.js b/api-sample/src/actions/users/fetchUserInvitation/queries/selectUserInvitation.js new file mode 100644 index 00000000..1ba779cf --- /dev/null +++ b/api-sample/src/actions/users/fetchUserInvitation/queries/selectUserInvitation.js @@ -0,0 +1,32 @@ +const { submitQuery, camelKeys, getFirst } = require("~root/lib/database"); + +const selectUserInvitation = ({ + email, + invitationId, + shortcode +}) => submitQuery` + SELECT + user_organization_invitations.user_organization_invitation_id, + user_organization_invitations.invitation_shortcode, + user_organization_invitations.organization_id, + user_organization_invitations.email, + user_organization_invitations.invited_by, + user_organization_invitations.user_organization_role_id, + user_organization_invitations.comment, + user_organization_invitations.sent_at, + user_organization_invitations.accepted_at, + user_organization_invitations.declined_at, + user_organization_invitations.updated_at, + users.first_name as invitedByFirstName, + users.last_name as invitedByLastName, + user_organization_roles.user_organization_role, + organizations.organization_name + FROM user_organization_invitations + LEFT JOIN users ON users.user_id = user_organization_invitations.invited_by + LEFT JOIN user_organization_roles ON user_organization_roles.user_organization_role_id = user_organization_invitations.user_organization_role_id + LEFT JOIN organizations ON organizations.organization_id = user_organization_invitations.organization_id + WHERE user_organization_invitations.email = ${email} && user_organization_invitations.user_organization_invitation_id = ${invitationId} && user_organization_invitations.invitation_shortcode = ${shortcode} + ORDER BY sent_at DESC +`; + +module.exports = getFirst(camelKeys(selectUserInvitation)); diff --git a/api-sample/src/actions/users/fetchUserInvitations/index.js b/api-sample/src/actions/users/fetchUserInvitations/index.js new file mode 100644 index 00000000..7a836e11 --- /dev/null +++ b/api-sample/src/actions/users/fetchUserInvitations/index.js @@ -0,0 +1,9 @@ +const selectUserInvitations = require("./queries/selectUserInvitations"); + +const fetchUserInvitations = async ({ email }) => { + const invitations = await selectUserInvitations({ email }); + + return { invitations }; +}; + +module.exports = fetchUserInvitations; diff --git a/api-sample/src/actions/users/fetchUserInvitations/queries/selectUserInvitations.js b/api-sample/src/actions/users/fetchUserInvitations/queries/selectUserInvitations.js new file mode 100644 index 00000000..1e4b8c82 --- /dev/null +++ b/api-sample/src/actions/users/fetchUserInvitations/queries/selectUserInvitations.js @@ -0,0 +1,28 @@ +const { submitQuery, camelKeys } = require("~root/lib/database"); + +const selectUserInvitations = ({ email }) => submitQuery` + SELECT + user_organization_invitations.user_organization_invitation_id, + user_organization_invitations.invitation_shortcode, + user_organization_invitations.organization_id, + user_organization_invitations.email, + user_organization_invitations.invited_by, + user_organization_invitations.user_organization_role_id, + user_organization_invitations.comment, + user_organization_invitations.sent_at, + user_organization_invitations.accepted_at, + user_organization_invitations.declined_at, + user_organization_invitations.updated_at, + users.first_name as invitedByFirstName, + users.last_name as invitedByLastName, + user_organization_roles.user_organization_role, + organizations.organization_name + FROM user_organization_invitations + LEFT JOIN users ON users.user_id = user_organization_invitations.invited_by + LEFT JOIN user_organization_roles ON user_organization_roles.user_organization_role_id = user_organization_invitations.user_organization_role_id + LEFT JOIN organizations ON organizations.organization_id = user_organization_invitations.organization_id + WHERE user_organization_invitations.email = ${email} + ORDER BY sent_at DESC +`; + +module.exports = camelKeys(selectUserInvitations); diff --git a/api-sample/src/actions/users/fetchUserOrganizations/index.js b/api-sample/src/actions/users/fetchUserOrganizations/index.js new file mode 100644 index 00000000..a3eb2554 --- /dev/null +++ b/api-sample/src/actions/users/fetchUserOrganizations/index.js @@ -0,0 +1,9 @@ +const selectUserOrganizations = require("./queries/selectUserOrganizations"); + +const fetchUserOrganizations = async ({ userId }) => { + const organizations = await selectUserOrganizations({ userId }); + + return { organizations }; +}; + +module.exports = fetchUserOrganizations; diff --git a/api-sample/src/actions/users/fetchUserOrganizations/queries/selectUserOrganizations.js b/api-sample/src/actions/users/fetchUserOrganizations/queries/selectUserOrganizations.js new file mode 100644 index 00000000..05f21ee2 --- /dev/null +++ b/api-sample/src/actions/users/fetchUserOrganizations/queries/selectUserOrganizations.js @@ -0,0 +1,16 @@ +const { submitQuery, camelKeys } = require("~root/lib/database"); + +const selectUserOrganizations = ({ userId }) => submitQuery` + SELECT + user_organizations.user_id, + organizations.organization_id, + user_organizations.user_organization_role_id, + user_organization_roles.user_organization_role, + organizations.organization_name + FROM user_organizations + LEFT JOIN organizations ON organizations.organization_id = user_organizations.organization_id + LEFT JOIN user_organization_roles ON user_organization_roles.user_organization_role_id = user_organizations.user_organization_role_id + WHERE user_id = ${userId} +`; + +module.exports = camelKeys(selectUserOrganizations); diff --git a/api-sample/src/actions/users/fetchUserTypes/index.js b/api-sample/src/actions/users/fetchUserTypes/index.js deleted file mode 100644 index e812aa83..00000000 --- a/api-sample/src/actions/users/fetchUserTypes/index.js +++ /dev/null @@ -1,9 +0,0 @@ -const selectUserTypes = require("./queries/selectUserTypes"); - -const fetchUserTypes = async () => { - const userTypes = await selectUserTypes(); - - return { userTypes }; -}; - -module.exports = fetchUserTypes; diff --git a/api-sample/src/actions/users/fetchUserTypes/queries/selectUserTypes.js b/api-sample/src/actions/users/fetchUserTypes/queries/selectUserTypes.js deleted file mode 100644 index c25429f9..00000000 --- a/api-sample/src/actions/users/fetchUserTypes/queries/selectUserTypes.js +++ /dev/null @@ -1,9 +0,0 @@ -const { submitQuery, camelKeys } = require("~root/lib/database"); - -const selectUserTypes = () => submitQuery` - SELECT - user_type - FROM user_types -`; - -module.exports = camelKeys(selectUserTypes); diff --git a/api-sample/src/actions/users/modifyRegistrationRequestStatus/index.js b/api-sample/src/actions/users/modifyRegistrationRequestStatus/index.js new file mode 100644 index 00000000..87c5d523 --- /dev/null +++ b/api-sample/src/actions/users/modifyRegistrationRequestStatus/index.js @@ -0,0 +1,10 @@ +const updateRegistrationRequestStatus = require("./queries/updateRegistrationRequestStatus"); + +const modifyRegistrationRequestStatus = async ({ registrationShortcode }) => { + const registrationRequestStatus = await updateRegistrationRequestStatus({ + registrationShortcode + }); + return { registrationRequestStatus }; +}; + +module.exports = modifyRegistrationRequestStatus; diff --git a/api-sample/src/actions/users/modifyRegistrationRequestStatus/queries/updateRegistrationRequestStatus.js b/api-sample/src/actions/users/modifyRegistrationRequestStatus/queries/updateRegistrationRequestStatus.js new file mode 100644 index 00000000..a566d589 --- /dev/null +++ b/api-sample/src/actions/users/modifyRegistrationRequestStatus/queries/updateRegistrationRequestStatus.js @@ -0,0 +1,12 @@ +const { submitQuery } = require("~root/lib/database"); + +const updateRegistrationRequestStatus = ({ + registrationShortcode +}) => submitQuery` + UPDATE registration_requests + SET status = "approved", + updated_at = CURRENT_TIMESTAMP + WHERE registration_shortcode= ${registrationShortcode}; +`; + +module.exports = updateRegistrationRequestStatus; diff --git a/api-sample/src/actions/users/modifyUser/queries/updateUserDetails.js b/api-sample/src/actions/users/modifyUser/queries/updateUserDetails.js index fdb033ef..6368ed95 100644 --- a/api-sample/src/actions/users/modifyUser/queries/updateUserDetails.js +++ b/api-sample/src/actions/users/modifyUser/queries/updateUserDetails.js @@ -7,10 +7,10 @@ const updateUserDetails = ({ password }) => submitQuery` UPDATE users - SET + SET first_name = ${firstName}, last_name = ${lastName}, - password= SHA2(CONCAT(${password}, ${process.env.PASSWORD_SALT}), 224) + password = SHA2(CONCAT(${password}, ${process.env.PASSWORD_SALT}), 224) WHERE user_id = ${userId}; `; diff --git a/api-sample/src/actions/users/modifyUserInvitation/index.js b/api-sample/src/actions/users/modifyUserInvitation/index.js new file mode 100644 index 00000000..881f9f22 --- /dev/null +++ b/api-sample/src/actions/users/modifyUserInvitation/index.js @@ -0,0 +1,12 @@ +const updateUserInvitation = require("./queries/updateUserInvitation"); + +const modifyUserInvitation = async ({ invitationId, shortcode }) => { + const patchedInvitationId = await updateUserInvitation({ + invitationId, + shortcode + }); + + return { patchedInvitationId }; +}; + +module.exports = modifyUserInvitation; diff --git a/api-sample/src/actions/users/modifyUserInvitation/queries/updateUserInvitation.js b/api-sample/src/actions/users/modifyUserInvitation/queries/updateUserInvitation.js new file mode 100644 index 00000000..ca59b576 --- /dev/null +++ b/api-sample/src/actions/users/modifyUserInvitation/queries/updateUserInvitation.js @@ -0,0 +1,28 @@ +const { + submitQuery, + sql, + sqlReduce, + getInsertId +} = require("~root/lib/database"); + +const updateUserInvitation = ({ invitationId, shortcode }) => { + const updates = []; + + if (invitationId && shortcode) { + updates.push(sql`accepted_at = CURRENT_TIMESTAMP`); + } + + if (updates.length !== 0) { + return submitQuery` + UPDATE + user_organization_invitations + SET + ${updates.reduce(sqlReduce)} + WHERE + user_organization_invitation_id = ${invitationId} && invitation_shortcode = ${shortcode} + `; + } + return Promise.resolve(); +}; + +module.exports = getInsertId(updateUserInvitation); diff --git a/api-sample/src/actions/users/removeInvitation/index.js b/api-sample/src/actions/users/removeInvitation/index.js new file mode 100644 index 00000000..62d89272 --- /dev/null +++ b/api-sample/src/actions/users/removeInvitation/index.js @@ -0,0 +1,10 @@ +const deleteInvitation = require("./queries/deleteInvitation"); + +const removeInvitation = async ({ invitationId }) => { + const invitation = await deleteInvitation({ + invitationId + }); + return { invitation }; +}; + +module.exports = removeInvitation; diff --git a/api-sample/src/actions/users/removeInvitation/queries/deleteInvitation.js b/api-sample/src/actions/users/removeInvitation/queries/deleteInvitation.js new file mode 100644 index 00000000..86d7d5cf --- /dev/null +++ b/api-sample/src/actions/users/removeInvitation/queries/deleteInvitation.js @@ -0,0 +1,8 @@ +const { submitQuery, getInsertId } = require("~root/lib/database"); + +const deleteInvitation = ({ invitationId }) => submitQuery` + DELETE + FROM user_organization_invitations + WHERE user_organization_invitation_id = ${invitationId} +`; +module.exports = getInsertId(deleteInvitation); diff --git a/api-sample/src/app/controllers/invitations/acceptInvitation/index.js b/api-sample/src/app/controllers/invitations/acceptInvitation/index.js new file mode 100644 index 00000000..6d96b809 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/acceptInvitation/index.js @@ -0,0 +1,48 @@ +const handleAPIError = require("~root/utils/handleAPIError"); +const fetchInvitationById = require("~root/actions/invitations/fetchInvitationById"); +const createOrganizationUser = require("~root/actions/organizations/createOrganizationUser"); +const patchInvitationAsAccepted = require("~root/actions/invitations/patchInvitationAsAccepted"); +const acceptInvitationSchema = require("./schemas/acceptInvitationSchema"); + +const acceptInvitation = async (req, res) => { + const { userId, email } = req.user; + const { shortCode, invitationId } = req.body; + + try { + await acceptInvitationSchema.validate( + { userId, invitationId, shortCode, email }, + { + abortEarly: false + } + ); + + const { invitation } = await fetchInvitationById({ + invitationId, + shortCode + }); + + const { + organizationId: orgId, + invitedBy, + userOrganizationRoleId + } = invitation; + + await patchInvitationAsAccepted({ + invitationId, + shortCode + }); + + const { organizationUserId } = await createOrganizationUser({ + newOrgUserUserId: userId, + orgId, + userId: invitedBy, + userOrganizationRoleId + }); + + res.status(201).send({ organizationUserId }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = acceptInvitation; diff --git a/api-sample/src/app/controllers/invitations/acceptInvitation/schemas/acceptInvitationSchema.js b/api-sample/src/app/controllers/invitations/acceptInvitation/schemas/acceptInvitationSchema.js new file mode 100644 index 00000000..6138c70f --- /dev/null +++ b/api-sample/src/app/controllers/invitations/acceptInvitation/schemas/acceptInvitationSchema.js @@ -0,0 +1,49 @@ +const yup = require("yup"); +const selectInvitationByShortCode = require("./queries/selectInvitationByShortCode"); + +const acceptInvitationSchema = yup.object().shape({ + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number"), + + email: yup + .string() + .email() + .label("email") + .typeError("The email must be a valid email"), + + invitationId: yup + .number() + .positive() + .label("Invitation Id") + .typeError("The invitation Id must be a valid number"), + + shortCode: yup + .string() + .min(1) + .label("Invitation ShortCode") + .typeError("The Invitation shortCode must be a valid string") + .test( + "ShortCodeMustExist", + "The Invitation ShortCode must belong to an invitation with the same Invitation Id & the Invitation must belong to the user", + async function test(shortCode) { + const { invitationId, email } = this.parent; + + const invitation = await selectInvitationByShortCode({ + shortCode, + invitationId, + email + }); + if (invitation) { + return true; + } + + return false; + } + ) +}); + +module.exports = acceptInvitationSchema; diff --git a/api-sample/src/app/controllers/invitations/acceptInvitation/schemas/queries/selectInvitationByShortCode.js b/api-sample/src/app/controllers/invitations/acceptInvitation/schemas/queries/selectInvitationByShortCode.js new file mode 100644 index 00000000..b4c046f3 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/acceptInvitation/schemas/queries/selectInvitationByShortCode.js @@ -0,0 +1,15 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectInvitationByShortCode = ({ + shortCode, + invitationId, + email +}) => submitQuery` + SELECT + invited_by, + email + FROM user_organization_invitations + WHERE user_organization_invitation_id = ${invitationId} && invitation_shortcode = ${shortCode} && email = ${email} +`; + +module.exports = getFirst(selectInvitationByShortCode); diff --git a/api-sample/src/app/controllers/invitations/deleteInvitation/index.js b/api-sample/src/app/controllers/invitations/deleteInvitation/index.js new file mode 100644 index 00000000..f7f3c192 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/deleteInvitation/index.js @@ -0,0 +1,32 @@ +const handleAPIError = require("~root/utils/handleAPIError"); +const removeInvitation = require("~root/actions/users/removeInvitation"); +const deleteInvitationSchema = require("./schemas/deleteInvitationSchema"); + +const deleteInvitation = async (req, res) => { + const { userId, email } = req.user; + const { shortcode, invitationId } = req.body; + + try { + await deleteInvitationSchema.validate( + { + userId, + invitationId, + shortcode, + email + }, + { + abortEarly: false + } + ); + const { invitation } = await removeInvitation({ + invitationId + }); + res.status(204).send({ + invitation + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = deleteInvitation; diff --git a/api-sample/src/app/controllers/invitations/deleteInvitation/schemas/deleteInvitationSchema.js b/api-sample/src/app/controllers/invitations/deleteInvitation/schemas/deleteInvitationSchema.js new file mode 100644 index 00000000..de91e76d --- /dev/null +++ b/api-sample/src/app/controllers/invitations/deleteInvitation/schemas/deleteInvitationSchema.js @@ -0,0 +1,48 @@ +const yup = require("yup"); +const selectInvitationByShortCode = require("./queries/selectInvitationByShortCode"); + +const deleteInvitationSchema = yup.object().shape({ + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number"), + + invitationId: yup + .string() + .label("invitaionId") + .typeError("The invitation id must be a valid id"), + + email: yup + .string() + .email() + .label("email") + .typeError("The email must be a valid email"), + + shortcode: yup + .string() + .min(1) + .label("Invitation ShortCode") + .typeError("The Invitation shortCode must be a valid string") + .test( + "ShortCodeMustExist", + "The Invitation ShortCode must belong to an invitation with the same Invitation Id & the Invitation must belong to the user", + async function test(shortCode) { + const { invitationId, email } = this.parent; + + const invitation = await selectInvitationByShortCode({ + shortCode, + invitationId, + email + }); + if (invitation) { + return true; + } + + return false; + } + ) +}); + +module.exports = deleteInvitationSchema; diff --git a/api-sample/src/app/controllers/invitations/deleteInvitation/schemas/queries/selectInvitationByShortCode.js b/api-sample/src/app/controllers/invitations/deleteInvitation/schemas/queries/selectInvitationByShortCode.js new file mode 100644 index 00000000..a3e65f38 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/deleteInvitation/schemas/queries/selectInvitationByShortCode.js @@ -0,0 +1,11 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUser = ({ shortCode, invitationId, email }) => submitQuery` + SELECT + invited_by, + email + FROM user_organization_invitations + WHERE user_organization_invitation_id = ${invitationId} && invitation_shortcode = ${shortCode} && email = ${email} +`; + +module.exports = getFirst(selectUser); diff --git a/api-sample/src/app/controllers/invitations/deleteInvitationByOrgUser/index.js b/api-sample/src/app/controllers/invitations/deleteInvitationByOrgUser/index.js new file mode 100644 index 00000000..36482be8 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/deleteInvitationByOrgUser/index.js @@ -0,0 +1,31 @@ +const removeInvitation = require("~root/actions/invitations/removeInvitation"); +const handleAPIError = require("~root/utils/handleAPIError"); +const deleteInvitationSchema = require("./schemas/deleteInvitationSchema"); + +const deleteInvitationByOrgUser = async (req, res) => { + const { userId } = req.user; + const { invitationId, orgId } = req.params; + + try { + await deleteInvitationSchema.validate( + { + orgId, + userId, + invitationId + }, + { + abortEarly: false + } + ); + const { invitation } = await removeInvitation({ + invitationId + }); + res.status(204).send({ + invitation + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = deleteInvitationByOrgUser; diff --git a/api-sample/src/app/controllers/invitations/deleteInvitationByOrgUser/schemas/deleteInvitationSchema.js b/api-sample/src/app/controllers/invitations/deleteInvitationByOrgUser/schemas/deleteInvitationSchema.js new file mode 100644 index 00000000..efbc2b31 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/deleteInvitationByOrgUser/schemas/deleteInvitationSchema.js @@ -0,0 +1,64 @@ +const yup = require("yup"); +// eslint-disable-next-line import/order +const selectInvitation = require("./queries/selectInvitation"); +const selectSuperAdmin = require("~root/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin"); +const selectOrganizationUser = require("~root/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser"); + +const deleteInvitationSchema = yup.object().shape({ + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test( + "doesUserBelongToOrganization", + "The user must belong to the Organization and have the appropriate access (organization admin or event manager) or be a superAdmin", + async function test(userId) { + const { orgId } = this.parent; + + const isUserSuperAdmin = await selectSuperAdmin({ + userId + }); + if (isUserSuperAdmin) { + return true; + } + + const user = await selectOrganizationUser({ + orgId, + userId + }); + if (user) { + return true; + } + + return false; + } + ), + + invitationId: yup + .number() + .positive() + .label("invitaionId") + .typeError("The invitation id must be a valid id") + .test( + "ShortCodeMustExist", + "The Invitation ShortCode must belong to an invitation with the same Invitation Id & the Invitation must belong to the user", + async function test(shortCode) { + const { invitationId, email } = this.parent; + + const invitation = await selectInvitation({ + shortCode, + invitationId, + email + }); + if (invitation) { + return true; + } + + return false; + } + ) +}); + +module.exports = deleteInvitationSchema; diff --git a/api-sample/src/app/controllers/invitations/deleteInvitationByOrgUser/schemas/queries/selectInvitation.js b/api-sample/src/app/controllers/invitations/deleteInvitationByOrgUser/schemas/queries/selectInvitation.js new file mode 100644 index 00000000..a4517584 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/deleteInvitationByOrgUser/schemas/queries/selectInvitation.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectInvitation = ({ invitationId }) => submitQuery` + SELECT + user_organization_invitation_id + FROM user_organization_invitations + WHERE user_organization_invitation_id = ${invitationId} +`; + +module.exports = getFirst(selectInvitation); diff --git a/api-sample/src/app/controllers/invitations/getUserInvitations/index.js b/api-sample/src/app/controllers/invitations/getUserInvitations/index.js new file mode 100644 index 00000000..dabf9c64 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/getUserInvitations/index.js @@ -0,0 +1,26 @@ +const fetchUserInvitations = require("~root/actions/users/fetchUserInvitations"); +const handleAPIError = require("~root/utils/handleAPIError"); +const getUserInvitationSchema = require("./schemas/getUserInvitationsSchema"); + +const getUserInvitations = async (req, res) => { + const { userId, email } = req.user; + try { + await getUserInvitationSchema.validate( + { + userId, + email + }, + { + abortEarly: false + } + ); + const { invitations } = await fetchUserInvitations({ email }); + res.status(201).send({ + invitations + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = getUserInvitations; diff --git a/api-sample/src/app/controllers/invitations/getUserInvitations/schemas/getUserInvitationsSchema.js b/api-sample/src/app/controllers/invitations/getUserInvitations/schemas/getUserInvitationsSchema.js new file mode 100644 index 00000000..48dab141 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/getUserInvitations/schemas/getUserInvitationsSchema.js @@ -0,0 +1,33 @@ +const yup = require("yup"); +const selectUser = require("./queries/selectUser"); + +const getUserInvitationsSchema = yup.object().shape({ + email: yup + .string() + .email() + .required() + .label("email") + .typeError("The email is invalid"), + + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test("doesUserExist", "The user must exist", async function test(userId) { + const { email } = this.parent; + + const user = await selectUser({ + userId, + email + }); + if (user) { + return true; + } + + return false; + }) +}); + +module.exports = getUserInvitationsSchema; diff --git a/api-sample/src/app/controllers/invitations/getUserInvitations/schemas/queries/selectUser.js b/api-sample/src/app/controllers/invitations/getUserInvitations/schemas/queries/selectUser.js new file mode 100644 index 00000000..152d3d7a --- /dev/null +++ b/api-sample/src/app/controllers/invitations/getUserInvitations/schemas/queries/selectUser.js @@ -0,0 +1,13 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUser = ({ userId, email }) => submitQuery` + SELECT + user_id, + first_name, + last_name, + email + FROM users + WHERE user_id = ${userId} && email = ${email} +`; + +module.exports = getFirst(selectUser); diff --git a/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/index.js b/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/index.js new file mode 100644 index 00000000..0f86a342 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/index.js @@ -0,0 +1,34 @@ +const handleAPIError = require("~root/utils/handleAPIError"); +const modifyOrganizationInvitation = require("~root/actions/invitations/modifyOrganizationInvitation"); +const patchOrganizationInvitationSchema = require("./schemas/patchOrganizationInvitationSchema"); + +const patchOrganizationInvitation = async (req, res) => { + const { userId } = req.user; + const { email, orgId, invitationShortcode } = req.body; + + try { + await patchOrganizationInvitationSchema.validate( + { + userId, + email, + orgId, + invitationShortcode + }, + { + abortEarly: false + } + ); + + const { orgInvitation } = await modifyOrganizationInvitation({ + email, + invitationShortcode, + orgId + }); + + res.status(201).send({ orgInvitation }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = patchOrganizationInvitation; diff --git a/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/schemas/patchOrganizationInvitationSchema.js b/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/schemas/patchOrganizationInvitationSchema.js new file mode 100644 index 00000000..0df99f1a --- /dev/null +++ b/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/schemas/patchOrganizationInvitationSchema.js @@ -0,0 +1,87 @@ +const yup = require("yup"); +const selectSuperAdmin = require("~root/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin"); +const selectOrganizationUser = require("~root/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser"); +const selectUserOrganizationByEmail = require("./queries/selectUserOrganizationByEmail"); +const selectInvitation = require("./queries/selectInvitation"); + +const patchOrganizationInvitationSchema = yup.object().shape({ + email: yup + .string() + .email() + .required() + .label("Email") + .typeError("Email is invalid.") + .test("doesEmailExist", "User account already exists.", async function test( + email + ) { + const { orgId } = this.parent; + + const userOrganization = await selectUserOrganizationByEmail({ + email, + orgId + }); + + if (userOrganization) { + return false; + } + + return true; + }), + + invitationShortcode: yup + .string() + .required() + .label("invitation shortcode") + .typeError("the invitation shortcode is not a valid string") + .test( + "doesInvitationShortcodeExist", + "Invitation shortcode does not exists.", + async function test(invitationShortcode) { + const invitation = await selectInvitation({ invitationShortcode }); + + if (invitation) { + return true; + } + return false; + } + ), + + orgId: yup + .number() + .required() + .label("Organization ID") + .typeError("Organization Id must be a number"), + + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test( + "doesUserBelongToOrganization", + "The user must belong to the Organization and have the appropriate access (organization admin or event manager) or be a superAdmin", + async function test(userId) { + const { orgId } = this.parent; + + const isUserSuperAdmin = await selectSuperAdmin({ + userId + }); + if (isUserSuperAdmin) { + return true; + } + + const user = await selectOrganizationUser({ + orgId, + userId + }); + if (user) { + return true; + } + + return false; + } + ) +}); + +module.exports = patchOrganizationInvitationSchema; diff --git a/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/schemas/queries/selectInvitation.js b/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/schemas/queries/selectInvitation.js new file mode 100644 index 00000000..d97ac968 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/schemas/queries/selectInvitation.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectInvitation = ({ invitationShortcode }) => submitQuery` + SELECT + user_organization_invitation_id + FROM user_organization_invitations + WHERE invitation_shortcode = ${invitationShortcode}; +`; + +module.exports = getFirst(selectInvitation, "user_organization_invitation_id"); diff --git a/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/schemas/queries/selectUserOrganizationByEmail.js b/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/schemas/queries/selectUserOrganizationByEmail.js new file mode 100644 index 00000000..cd6fe333 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/patchOrganizationInvitation/schemas/queries/selectUserOrganizationByEmail.js @@ -0,0 +1,14 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserOrganizationByEmail = ({ email, orgId }) => submitQuery` + SELECT + user_organizations.user_organization_id + FROM user_organizations + LEFT JOIN users ON user_organizations.user_id = users.user_id + WHERE users.email = ${email} && user_organizations.organization_id = ${orgId} +`; + +module.exports = getFirst( + selectUserOrganizationByEmail, + "user_organization_id" +); diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitation/index.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/index.js new file mode 100644 index 00000000..64132f96 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/index.js @@ -0,0 +1,94 @@ +const { v4: uuidv4 } = require("uuid"); +const handleAPIError = require("~root/utils/handleAPIError"); +const createOrganizationInvitation = require("~root/actions/users/createOrganizationInvitation"); +const sendEmail = require("~root/services/emails/sendEmail"); +const postOrganizationInvitationSchema = require("./schemas/postOrganizationInvitationSchema"); +const selectUserFullNameById = require("./queries/selectUserFullNameById"); +const selectOrganizationNameById = require("./queries/selectOrganizationNameById"); +const selectUserOrganizationRoleNameById = require("./queries/selectUserOrganizationRoleNameById"); +const selectUserByEmail = require("./queries/selectUserByEmail"); + +const postOrganizationInvitation = async (req, res) => { + const { userId } = req.user; + const { email, userOrganizationRoleId, comment, orgId } = req.body; + const invitationShortcode = uuidv4(); + + try { + await postOrganizationInvitationSchema.validate( + { + userId, + orgId, + email, + userOrganizationRoleId, + comment + }, + { + abortEarly: false + } + ); + + const { firstName, lastName } = await selectUserFullNameById({ userId }); + const { organizationName } = await selectOrganizationNameById({ orgId }); + const { userOrganizationRole } = await selectUserOrganizationRoleNameById({ + userOrganizationRoleId + }); + const { user } = await selectUserByEmail({ email }); + + let newRole; + if (userOrganizationRole === "OrganizationAdmin") { + newRole = "Organization Admin"; + } + if (userOrganizationRole === "Member") { + newRole = "Member"; + } + + const { newInvitationId } = await createOrganizationInvitation({ + userId, + orgId, + email, + userOrganizationRoleId, + comment, + invitationShortcode + }); + + if (user) { + const emailPayload = { + to: email, + template: "user-organization-invitation", + version: "0.0.1", + metadata: { + firstName, + lastName, + userOrganizationRole: newRole, + organizationName, + senderMessage: comment, + notificationsPageUrl: `${process.env.APP_BASE_URL}/login` + } + }; + await sendEmail(emailPayload); + } else { + const emailPayload = { + to: email, + template: "new-user-organization-invitation", + version: "0.0.1", + metadata: { + firstName, + lastName, + userOrganizationRole: newRole, + organizationName, + senderMessage: comment, + notificationsPageUrl: `${process.env.APP_BASE_URL}/register/${invitationShortcode}` + } + }; + await sendEmail(emailPayload); + } + + res.send({ + newInvitationId + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = postOrganizationInvitation; diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectOrganizationNameById.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectOrganizationNameById.js new file mode 100644 index 00000000..b7a1eeeb --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectOrganizationNameById.js @@ -0,0 +1,12 @@ +const { submitQuery, getFirst, camelKeys } = require("~root/lib/database"); + +const selectUserOrganizationByEmail = ({ orgId }) => submitQuery` + SELECT + organization_name + FROM organizations + WHERE organization_id = ${orgId} +`; + +module.exports = getFirst( + camelKeys(selectUserOrganizationByEmail, "organization_name") +); diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectUserByEmail.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectUserByEmail.js new file mode 100644 index 00000000..50c9579a --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectUserByEmail.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserByEmail = ({ email }) => submitQuery` + SELECT + email + FROM users + WHERE email = ${email}; +`; + +module.exports = getFirst(selectUserByEmail, "email"); diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectUserFullNameById.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectUserFullNameById.js new file mode 100644 index 00000000..b9a3106a --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectUserFullNameById.js @@ -0,0 +1,11 @@ +const { submitQuery, getFirst, camelKeys } = require("~root/lib/database"); + +const selectUserFullNameById = ({ userId }) => submitQuery` + SELECT + first_name, + last_name + FROM users + WHERE user_id = ${userId} +`; + +module.exports = getFirst(camelKeys(selectUserFullNameById)); diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectUserOrganizationRoleNameById.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectUserOrganizationRoleNameById.js new file mode 100644 index 00000000..76e2b85c --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/queries/selectUserOrganizationRoleNameById.js @@ -0,0 +1,12 @@ +const { submitQuery, getFirst, camelKeys } = require("~root/lib/database"); + +const selectUserOrganizationRoleNameById = ({ + userOrganizationRoleId +}) => submitQuery` + SELECT + user_organization_role + FROM user_organization_roles + WHERE user_organization_role_id = ${userOrganizationRoleId} +`; + +module.exports = getFirst(camelKeys(selectUserOrganizationRoleNameById)); diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/postOrganizationInvitationSchema.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/postOrganizationInvitationSchema.js new file mode 100644 index 00000000..6fcd6383 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/postOrganizationInvitationSchema.js @@ -0,0 +1,101 @@ +const yup = require("yup"); +const selectSuperAdmin = require("~root/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin"); +const selectOrganizationUser = require("~root/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser"); +const selectUserOrganizationByEmail = require("./queries/selectUserOrganizationByEmail"); +const selectUserRole = require("./queries/selectUserRole"); +const selectInvitation = require("./queries/selectInvitation"); + +const postOrganizationInvitationSchema = yup.object().shape({ + comment: yup + .string() + .label("comment") + .typeError("Invalid comment") + .max(500), + + email: yup + .string() + .email() + .required() + .label("Email") + .typeError("Email is invalid.") + .test( + "doesEmailExist", + "User account already exists or Invitation already exist", + async function test(email) { + const { orgId, userOrganizationRoleId } = this.parent; + + const userOrganization = await selectUserOrganizationByEmail({ + email, + orgId, + userOrganizationRoleId + }); + if (userOrganization) { + return false; + } + + const invitation = await selectInvitation({ + email, + orgId, + userOrganizationRoleId + }); + + if (invitation) { + return false; + } + return true; + } + ), + + userOrganizationRoleId: yup + .number() + .positive() + .required() + .label("user role id") + .typeError("the user role id must be a valid number") + .test( + "doesUserRoleIdExist", + "User role Id does not exists.", + async function test(userOrganizationRoleId) { + const userRole = await selectUserRole({ userOrganizationRoleId }); + + if (userRole) { + return true; + } + return false; + } + ), + orgId: yup + .number() + .required() + .label("Organization ID") + .typeError("Organization Id must be a number"), + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test( + "doesUserBelongToOrganization", + "The user must belong to the Organization and have the appropriate access (organization admin or event manager) or be a superAdmin", + async function test(userId) { + const { orgId } = this.parent; + const isUserSuperAdmin = await selectSuperAdmin({ + userId + }); + if (isUserSuperAdmin) { + return true; + } + const user = await selectOrganizationUser({ + orgId, + userId + }); + if (user) { + return true; + } + return false; + } + ) +}); + +module.exports = postOrganizationInvitationSchema; diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/queries/selectInvitation.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/queries/selectInvitation.js new file mode 100644 index 00000000..119872ee --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/queries/selectInvitation.js @@ -0,0 +1,14 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectInvitation = ({ + email, + orgId, + userOrganizationRoleId +}) => submitQuery` + SELECT + user_organization_invitation_id + FROM user_organization_invitations + WHERE email = ${email} && organization_id = ${orgId} && user_organization_role_id = ${userOrganizationRoleId} +`; + +module.exports = getFirst(selectInvitation, "user_organization_invitation_id"); diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/queries/selectUserOrganizationByEmail.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/queries/selectUserOrganizationByEmail.js new file mode 100644 index 00000000..f04463d9 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/queries/selectUserOrganizationByEmail.js @@ -0,0 +1,18 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserOrganizationByEmail = ({ + email, + orgId, + userOrganizationRoleId +}) => submitQuery` + SELECT + user_organizations.user_organization_id + FROM user_organizations + LEFT JOIN users ON users.user_id = user_organizations.user_id + WHERE users.email = ${email} && user_organizations.organization_id = ${orgId} && user_organizations.user_organization_role_id = ${userOrganizationRoleId} +`; + +module.exports = getFirst( + selectUserOrganizationByEmail, + "user_organization_id" +); diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/queries/selectUserRole.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/queries/selectUserRole.js new file mode 100644 index 00000000..cc9439e1 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitation/schemas/queries/selectUserRole.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserRole = ({ userOrganizationRoleId }) => submitQuery` + SELECT + user_organization_role + FROM user_organization_roles + WHERE user_organization_role_id = ${userOrganizationRoleId} +`; + +module.exports = getFirst(selectUserRole, "user_organization_role"); diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitations/index.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitations/index.js new file mode 100644 index 00000000..6bb2e8b5 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitations/index.js @@ -0,0 +1,48 @@ +const { v4: uuidv4 } = require("uuid"); +const handleAPIError = require("~root/utils/handleAPIError"); +const createOrganizationInvitation = require("~root/actions/invitations/createOrganizationInvitation"); +const asyncParallel = require("~root/utils/asyncParallel"); +const postOrganizationInvitationsSchema = require("./schemas/postOrganizationInvitationsSchema"); + +const postOrganizationInvitations = async (req, res) => { + const { userId } = req.user; + const { orgId } = req.params; + const { allInvitedUsers } = req.body; + const invitationShortcode = uuidv4(); + + try { + const invitedUsers = []; + await asyncParallel(allInvitedUsers, async newUser => { + const { email, userOrganizationRoleId } = newUser; + + await postOrganizationInvitationsSchema.validate( + { + userId, + orgId, + email, + userOrganizationRoleId + }, + { + abortEarly: false + } + ); + + const { newInvitationId } = await createOrganizationInvitation({ + userId, + orgId, + email, + userOrganizationRoleId, + invitationShortcode + }); + invitedUsers.push(newInvitationId); + }); + + res.send({ + invitedUsers + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = postOrganizationInvitations; diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitations/schemas/postOrganizationInvitationsSchema.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitations/schemas/postOrganizationInvitationsSchema.js new file mode 100644 index 00000000..3ba2de7e --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitations/schemas/postOrganizationInvitationsSchema.js @@ -0,0 +1,87 @@ +const yup = require("yup"); +const selectSuperAdmin = require("~root/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin"); +const selectOrganizationUser = require("~root/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser"); +const selectUserOrganizationByEmail = require("./queries/selectUserOrganizationByEmail"); +const selectUserRole = require("./queries/selectUserRole"); + +const postOrganizationInvitationsSchema = yup.object().shape({ + email: yup + .string() + .email() + .required() + .label("Email") + .typeError("Email is invalid.") + .test("doesEmailExist", "User account already exists.", async function test( + email + ) { + const { orgId } = this.parent; + + const userOrganization = await selectUserOrganizationByEmail({ + email, + orgId + }); + + if (userOrganization) { + return false; + } + + return true; + }), + + userOrganizationRoleId: yup + .number() + .positive() + .required() + .label("user role id") + .typeError("the user role id must be a valid number") + .test( + "doesUserRoleIdExist", + "User role Id does not exists.", + async function test(userOrganizationRoleId) { + const userRole = await selectUserRole({ userOrganizationRoleId }); + if (userRole) { + return true; + } + return false; + } + ), + + orgId: yup + .number() + .required() + .label("Organization ID") + .typeError("Organization Id must be a number"), + + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test( + "doesUserBelongToOrganization", + "The user must belong to the Organization and have the appropriate access (organization admin or event manager) or be a superAdmin", + async function test(userId) { + const { orgId } = this.parent; + + const isUserSuperAdmin = await selectSuperAdmin({ + userId + }); + if (isUserSuperAdmin) { + return true; + } + + const user = await selectOrganizationUser({ + orgId, + userId + }); + if (user) { + return true; + } + + return false; + } + ) +}); + +module.exports = postOrganizationInvitationsSchema; diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitations/schemas/queries/selectUserOrganizationByEmail.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitations/schemas/queries/selectUserOrganizationByEmail.js new file mode 100644 index 00000000..2fe76985 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitations/schemas/queries/selectUserOrganizationByEmail.js @@ -0,0 +1,14 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserOrganizationByEmail = ({ email, orgId }) => submitQuery` + SELECT + user_organizations.user_organization_id + FROM user_organizations + LEFT JOIN users ON users.user_id = user_organizations.user_id + WHERE users.email = ${email} && user_organizations.organization_id = ${orgId} +`; + +module.exports = getFirst( + selectUserOrganizationByEmail, + "user_organization_id" +); diff --git a/api-sample/src/app/controllers/invitations/postOrganizationInvitations/schemas/queries/selectUserRole.js b/api-sample/src/app/controllers/invitations/postOrganizationInvitations/schemas/queries/selectUserRole.js new file mode 100644 index 00000000..cc9439e1 --- /dev/null +++ b/api-sample/src/app/controllers/invitations/postOrganizationInvitations/schemas/queries/selectUserRole.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserRole = ({ userOrganizationRoleId }) => submitQuery` + SELECT + user_organization_role + FROM user_organization_roles + WHERE user_organization_role_id = ${userOrganizationRoleId} +`; + +module.exports = getFirst(selectUserRole, "user_organization_role"); diff --git a/api-sample/src/app/controllers/organizations/deleteOrganizationUser/index.js b/api-sample/src/app/controllers/organizations/deleteOrganizationUser/index.js new file mode 100644 index 00000000..d24c72f8 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/deleteOrganizationUser/index.js @@ -0,0 +1,32 @@ +const handleAPIError = require("~root/utils/handleAPIError"); +const removeOrganizationUser = require("~root/actions/organizations/removeOrganizationUser"); +const deleteOrganizationUserSchema = require("./schemas/deleteOrganizationUserSchema"); + +const deleteOrganizationUser = async (req, res) => { + const { userId } = req.user; + const { orgId, orgUserId } = req.body; + + try { + await deleteOrganizationUserSchema.validate( + { + orgId, + userId, + orgUserId + }, + { + abortEarly: false + } + ); + + const { deletedOrgUser } = await removeOrganizationUser({ + orgUserId, + orgId + }); + + res.status(204).send({ deletedOrgUser }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = deleteOrganizationUser; diff --git a/api-sample/src/app/controllers/organizations/deleteOrganizationUser/schemas/deleteOrganizationUserSchema.js b/api-sample/src/app/controllers/organizations/deleteOrganizationUser/schemas/deleteOrganizationUserSchema.js new file mode 100644 index 00000000..aac9b784 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/deleteOrganizationUser/schemas/deleteOrganizationUserSchema.js @@ -0,0 +1,65 @@ +const yup = require("yup"); +const selectOrganizationUser = require("~root/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser"); +const selectSuperAdmin = require("~root/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin"); +const selectOrgUserById = require("./queries/selectOrgUserById"); + +const deleteOrganizationUserSchema = yup.object().shape({ + orgId: yup + .number() + .required() + .label("Organization ID") + .typeError("Organization Id must be a number"), + + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test( + "doesUserBelongToOrganization", + "The user must belong to the Organization and have the appropriate access (organization admin or event manager) or be a superAdmin", + async function test(userId) { + const { orgId } = this.parent; + + const isUserSuperAdmin = await selectSuperAdmin({ + userId + }); + if (isUserSuperAdmin) { + return true; + } + + const user = await selectOrganizationUser({ + orgId, + userId + }); + if (user) { + return true; + } + + return false; + } + ), + + orgUserId: yup + .number() + .positive() + .required() + .label("OrgUser to be deleted") + .typeError("the organization User Id must be a valid number") + .test( + "doesTheUserBelongToTheOrganization", + "check if the organization user exist and belong to the organization", + async function test(orgUserId) { + const { orgId } = this.parent; + const orgUser = await selectOrgUserById({ orgId, orgUserId }); + + if (orgUser) { + return true; + } + return false; + } + ) +}); + +module.exports = deleteOrganizationUserSchema; diff --git a/api-sample/src/app/controllers/organizations/deleteOrganizationUser/schemas/queries/selectOrgUserById.js b/api-sample/src/app/controllers/organizations/deleteOrganizationUser/schemas/queries/selectOrgUserById.js new file mode 100644 index 00000000..ed086ca2 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/deleteOrganizationUser/schemas/queries/selectOrgUserById.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectOrgUserById = ({ orgId, orgUserId }) => submitQuery` + SELECT + user_organization_id + FROM user_organizations + WHERE user_id = ${orgUserId} && organization_id = ${orgId} +`; + +module.exports = getFirst(selectOrgUserById, "user_organization_id"); diff --git a/api-sample/src/app/controllers/organizations/getOrganizationInvitations/index.js b/api-sample/src/app/controllers/organizations/getOrganizationInvitations/index.js new file mode 100644 index 00000000..f1d28d85 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/getOrganizationInvitations/index.js @@ -0,0 +1,29 @@ +const fetchOrganizationInvitations = require("~root/actions/organizations/fetchOrganizationInvitations"); +const handleAPIError = require("~root/utils/handleAPIError"); +const getOrganizationInvitationsSchema = require("./schemas/getOrganizationInvitationsSchema"); + +const getOrganizationInvitations = async (req, res) => { + const { userId } = req.user; + const { orgId } = req.params; + + try { + await getOrganizationInvitationsSchema.validate( + { + orgId, + userId + }, + { + abortEarly: false + } + ); + const { invitations } = await fetchOrganizationInvitations({ orgId }); + + res.status(201).send({ + invitations + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = getOrganizationInvitations; diff --git a/api-sample/src/app/controllers/organizations/getOrganizationInvitations/schemas/getOrganizationInvitationsSchema.js b/api-sample/src/app/controllers/organizations/getOrganizationInvitations/schemas/getOrganizationInvitationsSchema.js new file mode 100644 index 00000000..debdfd42 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/getOrganizationInvitations/schemas/getOrganizationInvitationsSchema.js @@ -0,0 +1,39 @@ +const yup = require("yup"); +const selectUserByOrgId = require("~root/actions/schemaHelpers/queries/selectUserById/selectUserByOrgId"); +const selectUserById = require("~root/actions/schemaHelpers/queries/selectUserById/selectUserById"); + +const getOrganizationInvitationsSchema = yup.object().shape({ + orgId: yup + .number() + .positive() + .label("OrganizationId") + .typeError("Organization Id must be valid."), + userId: yup + .number() + .positive() + .label("userId") + .typeError("The userId must be a number") + .test( + "UserMustExistAndBelongToThatOrganization", + "The user does not exist or does not belong to that organization", + async function test(userId) { + const { orgId } = this.parent; + + const user = await selectUserById({ userId }); + + if (!user) { + return false; + } + + const orgUser = await selectUserByOrgId({ userId, orgId }); + + if (!orgUser) { + return false; + } + + return true; + } + ) +}); + +module.exports = getOrganizationInvitationsSchema; diff --git a/api-sample/src/app/controllers/organizations/getOrganizationUsers/index.js b/api-sample/src/app/controllers/organizations/getOrganizationUsers/index.js new file mode 100644 index 00000000..8e350c98 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/getOrganizationUsers/index.js @@ -0,0 +1,28 @@ +const handleAPIError = require("~root/utils/handleAPIError"); +const fetchOrganizationUsers = require("~root/actions/organizations/fetchOrganizationUsers"); +const getOrganizationUsersSchema = require("./schemas/getOrganizationUsersSchema"); + +const getOrganizationUsers = async (req, res) => { + const { userId } = req.user; + const { orgId } = req.params; + try { + await getOrganizationUsersSchema.validate( + { + orgId, + userId + }, + { + abortEarly: false + } + ); + const { organizationUsers } = await fetchOrganizationUsers({ orgId }); + + res.status(200).send({ + organizationUsers + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = getOrganizationUsers; diff --git a/api-sample/src/app/controllers/organizations/getOrganizationUsers/schemas/getOrganizationUsersSchema.js b/api-sample/src/app/controllers/organizations/getOrganizationUsers/schemas/getOrganizationUsersSchema.js new file mode 100644 index 00000000..d67689eb --- /dev/null +++ b/api-sample/src/app/controllers/organizations/getOrganizationUsers/schemas/getOrganizationUsersSchema.js @@ -0,0 +1,35 @@ +const yup = require("yup"); +const selectUserByOrgId = require("~root/actions/schemaHelpers/queries/selectUserById/selectUserByOrgId"); +const selectUserById = require("~root/actions/users/fetchUserById/queries/selectUserById"); + +const getOrganizationUsersSchema = yup.object().shape({ + orgId: yup + .number() + .positive() + .label("OrganizationId") + .typeError("Organization Id must be valid."), + + userId: yup + .number() + .positive() + .label("userId") + .typeError("The userId must be a number") + .test( + "UserMustExistAndBelongToThatOrganization", + "The user does not exist or does not belong to that organization", + async function test(userId) { + const { orgId } = this.parent; + const user = await selectUserById({ userId }); + if (!user) { + return false; + } + const orgUser = await selectUserByOrgId({ userId, orgId }); + if (!orgUser) { + return false; + } + return true; + } + ) +}); + +module.exports = getOrganizationUsersSchema; diff --git a/api-sample/src/app/controllers/organizations/getOrganizationsByEmail/index.js b/api-sample/src/app/controllers/organizations/getOrganizationsByEmail/index.js new file mode 100644 index 00000000..bca31b92 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/getOrganizationsByEmail/index.js @@ -0,0 +1,26 @@ +const fetchOrganizationsByEmail = require("~root/actions/organizations/fetchOrganizationsByEmail"); +const handleAPIError = require("~root/utils/handleAPIError"); +const getOrgOverviewSchema = require("./schemas/getOrganizationsByEmailSchema"); + +const getOrganizationsByEmail = async (req, res) => { + const { email } = req.params; + + try { + await getOrgOverviewSchema.validate( + { + email + }, + { + abortEarly: false + } + ); + const { organizations } = await fetchOrganizationsByEmail({ email }); + res.status(200).send({ + organizations + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = getOrganizationsByEmail; diff --git a/api-sample/src/app/controllers/organizations/getOrganizationsByEmail/schemas/getOrganizationsByEmailSchema.js b/api-sample/src/app/controllers/organizations/getOrganizationsByEmail/schemas/getOrganizationsByEmailSchema.js new file mode 100644 index 00000000..43d6e5ae --- /dev/null +++ b/api-sample/src/app/controllers/organizations/getOrganizationsByEmail/schemas/getOrganizationsByEmailSchema.js @@ -0,0 +1,26 @@ +const yup = require("yup"); +const selectUserByEmail = require("./queries/selectUserByEmail"); + +const getOrganizationsByEmailSchema = yup.object().shape({ + email: yup + .string() + .email() + .label("email") + .typeError("Not a valid Email") + .test( + "DoesEmailExist", + "The Email address does not exist for any user", + async function test(email) { + const user = await selectUserByEmail({ + email + }); + if (user) { + return true; + } + + return false; + } + ) +}); + +module.exports = getOrganizationsByEmailSchema; diff --git a/api-sample/src/app/controllers/organizations/getOrganizationsByEmail/schemas/queries/selectUserByEmail.js b/api-sample/src/app/controllers/organizations/getOrganizationsByEmail/schemas/queries/selectUserByEmail.js new file mode 100644 index 00000000..9264495f --- /dev/null +++ b/api-sample/src/app/controllers/organizations/getOrganizationsByEmail/schemas/queries/selectUserByEmail.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserByEmail = ({ email }) => submitQuery` + SELECT + user_id + FROM users + WHERE email = ${email} +`; + +module.exports = getFirst(selectUserByEmail, "user_id"); diff --git a/api-sample/src/app/controllers/organizations/getOrganizationsByUserId/index.js b/api-sample/src/app/controllers/organizations/getOrganizationsByUserId/index.js new file mode 100644 index 00000000..e50e7f08 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/getOrganizationsByUserId/index.js @@ -0,0 +1,25 @@ +const fetchOrganizationsByUserId = require("~root/actions/organizations/fetchOrganizationsByUserId"); +const handleAPIError = require("~root/utils/handleAPIError"); +const getOrganizationsByUserIdSchema = require("./schemas/getOrganizationsByUserIdSchema"); + +const getOrganizationsByUserId = async (req, res) => { + const { userId } = req.user; + try { + await getOrganizationsByUserIdSchema.validate( + { + userId + }, + { + abortEarly: false + } + ); + const { organizations } = await fetchOrganizationsByUserId({ userId }); + res.status(200).send({ + organizations + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = getOrganizationsByUserId; diff --git a/api-sample/src/app/controllers/organizations/getOrganizationsByUserId/schemas/getOrganizationsByUserIdSchema.js b/api-sample/src/app/controllers/organizations/getOrganizationsByUserId/schemas/getOrganizationsByUserIdSchema.js new file mode 100644 index 00000000..ce60cabd --- /dev/null +++ b/api-sample/src/app/controllers/organizations/getOrganizationsByUserId/schemas/getOrganizationsByUserIdSchema.js @@ -0,0 +1,23 @@ +const yup = require("yup"); +const selectUser = require("./queries/selectUser"); + +const getOrganizationsByUserIdSchema = yup.object().shape({ + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test("doesUserExist", "The user must exist", async function test(userId) { + const user = await selectUser({ + userId + }); + if (user) { + return true; + } + + return false; + }) +}); + +module.exports = getOrganizationsByUserIdSchema; diff --git a/api-sample/src/app/controllers/organizations/getOrganizationsByUserId/schemas/queries/selectUser.js b/api-sample/src/app/controllers/organizations/getOrganizationsByUserId/schemas/queries/selectUser.js new file mode 100644 index 00000000..fd48bc18 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/getOrganizationsByUserId/schemas/queries/selectUser.js @@ -0,0 +1,13 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUser = ({ userId }) => submitQuery` + SELECT + user_id, + first_name, + last_name, + email + FROM users + WHERE user_id = ${userId} +`; + +module.exports = getFirst(selectUser); diff --git a/api-sample/src/app/controllers/organizations/patchOrganizationUser/index.js b/api-sample/src/app/controllers/organizations/patchOrganizationUser/index.js new file mode 100644 index 00000000..7c9a0ada --- /dev/null +++ b/api-sample/src/app/controllers/organizations/patchOrganizationUser/index.js @@ -0,0 +1,36 @@ +const modifyOrganizationUser = require("~root/actions/organizations/modifyOrganizationUser"); +const handleAPIError = require("~root/utils/handleAPIError"); +const patchOrganizationUserSchema = require("./schemas/patchOrganizationUserSchema"); + +const patchOrganizationUser = async (req, res) => { + const { userId } = req.user; + const { orgId, userId: orgUserId } = req.params; + const { userRoleId, enabled } = req.body; + + try { + await patchOrganizationUserSchema.validate( + { + userId, + orgId, + orgUserId, + userRoleId, + enabled + }, + { + abortEarly: false + } + ); + const { updatedorgUserId } = await modifyOrganizationUser({ + userId, + orgId, + orgUserId, + userRoleId, + enabled + }); + res.status(201).send({ updatedorgUserId }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = patchOrganizationUser; diff --git a/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/patchOrganizationUserSchema.js b/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/patchOrganizationUserSchema.js new file mode 100644 index 00000000..f031cf4d --- /dev/null +++ b/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/patchOrganizationUserSchema.js @@ -0,0 +1,89 @@ +const yup = require("yup"); +const selectSuperAdmin = require("~root/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin"); +const selectOrganizationUser = require("~root/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser"); +const selectOrgUserByUserIdAndOrgId = require("./queries/selectOrgUserByUserIdAndOrgId"); +const selectUserRole = require("./queries/selectUserRole"); + +const patchOrganizationUserSchema = yup.object().shape({ + orgId: yup + .number() + .required() + .label("Organization ID") + .typeError("Organization Id must be a number"), + + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test( + "doesUserBelongToOrganization", + "The user must belong to the Organization and have the appropriate access (organization admin or event manager) or be a superAdmin", + async function test(userId) { + const { orgId } = this.parent; + + const isUserSuperAdmin = await selectSuperAdmin({ + userId + }); + if (isUserSuperAdmin) { + return true; + } + + const user = await selectOrganizationUser({ + orgId, + userId + }); + if (user) { + return true; + } + + return false; + } + ), + + orgUserId: yup + .number() + .positive() + .label("Org User Id") + .typeError("The orgUserId must be a valid number") + .test( + "orgUserIdMustExist", + "The orgUserId must exist in the user_organization table", + async function test(orgUserId) { + const { orgId } = this.parent; + const orgUser = await selectOrgUserByUserIdAndOrgId({ + orgUserId, + orgId + }); + if (orgUser) { + return true; + } + return false; + } + ), + + userRoleId: yup + .number() + .positive() + .label("userRoleId") + .typeError("The eventRoleId must a valid number") + .test( + "DoesuserRoleIdExists", + "The userRoleId must exist in the user_roles table", + async function test(userRoleId) { + const userRole = await selectUserRole({ userRoleId }); + if (userRole) { + return true; + } + return false; + } + ), + + enabled: yup + .boolean() + .label("enabled") + .typeError("enabled must be a boolean") +}); + +module.exports = patchOrganizationUserSchema; diff --git a/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/queries/selectEventByOrgId.js b/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/queries/selectEventByOrgId.js new file mode 100644 index 00000000..dff9c45c --- /dev/null +++ b/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/queries/selectEventByOrgId.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectEventByOrgId = ({ eventId, orgId }) => submitQuery` + SELECT + event_id + FROM events + WHERE event_id = ${eventId} && organization_id = ${orgId} +`; + +module.exports = getFirst(selectEventByOrgId, "event_id"); diff --git a/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/queries/selectOrgUserByUserIdAndOrgId.js b/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/queries/selectOrgUserByUserIdAndOrgId.js new file mode 100644 index 00000000..3fb63034 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/queries/selectOrgUserByUserIdAndOrgId.js @@ -0,0 +1,13 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectOrgUserByUserIdAndOrgId = ({ orgUserId, orgId }) => submitQuery` + SELECT + user_organization_id + FROM user_organizations + WHERE user_id = ${orgUserId} && organization_id = ${orgId} +`; + +module.exports = getFirst( + selectOrgUserByUserIdAndOrgId, + "user_organization_id" +); diff --git a/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/queries/selectUserRole.js b/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/queries/selectUserRole.js new file mode 100644 index 00000000..1251374a --- /dev/null +++ b/api-sample/src/app/controllers/organizations/patchOrganizationUser/schemas/queries/selectUserRole.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserRole = ({ userRoleId }) => submitQuery` + SELECT + user_role + FROM user_roles + WHERE user_role_id = ${userRoleId} +`; + +module.exports = getFirst(selectUserRole, "user_role"); diff --git a/api-sample/src/app/controllers/organizations/postNewOrganization/index.js b/api-sample/src/app/controllers/organizations/postNewOrganization/index.js new file mode 100644 index 00000000..10d9937e --- /dev/null +++ b/api-sample/src/app/controllers/organizations/postNewOrganization/index.js @@ -0,0 +1,27 @@ +const createNewOrganization = require("~root/actions/organizations/createNewOrganization"); +const handleAPIError = require("~root/utils/handleAPIError"); +const postOrganizationSchema = require("./schemas/postOrganizationSchema"); + +const postNewOrganization = async (req, res) => { + const { userId } = req.user; + const { organizationName } = req.body; + try { + await postOrganizationSchema.validate( + { + userId, + organizationName + }, + { + abortEarly: false + } + ); + const { organizationId } = await createNewOrganization({ + organizationName + }); + res.status(201).send({ organizationId }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = postNewOrganization; diff --git a/api-sample/src/app/controllers/organizations/postNewOrganization/schemas/postOrganizationSchema.js b/api-sample/src/app/controllers/organizations/postNewOrganization/schemas/postOrganizationSchema.js new file mode 100644 index 00000000..36f59dd9 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/postNewOrganization/schemas/postOrganizationSchema.js @@ -0,0 +1,19 @@ +const yup = require("yup"); + +const postOrganizationSchema = yup.object().shape({ + organizationName: yup + .string() + .min(1, "This field must not be empty") + .max(100, "The number of characters should not be more than 100") + .label("organization name") + .typeError("The organization name must be a valid string"), + + userId: yup + .number() + .positive() + .required() + .label("User Id") + .typeError("The user id must be a valid number") +}); + +module.exports = postOrganizationSchema; diff --git a/api-sample/src/app/controllers/organizations/postNewOrganization/schemas/queries/selectOrganizationByName.js b/api-sample/src/app/controllers/organizations/postNewOrganization/schemas/queries/selectOrganizationByName.js new file mode 100644 index 00000000..05572123 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/postNewOrganization/schemas/queries/selectOrganizationByName.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectOrganizationByName = ({ organizationName }) => submitQuery` + SELECT + organization_id + FROM organizations + WHERE organization_name = ${organizationName} +`; + +module.exports = getFirst(selectOrganizationByName, "organization_id"); diff --git a/api-sample/src/app/controllers/organizations/postOrganization/index.js b/api-sample/src/app/controllers/organizations/postOrganization/index.js new file mode 100644 index 00000000..25951ffd --- /dev/null +++ b/api-sample/src/app/controllers/organizations/postOrganization/index.js @@ -0,0 +1,39 @@ +const handleAPIError = require("~root/utils/handleAPIError"); +const createOrganization = require("~root/actions/organizations/createOrganization"); +const createOrganizationUser = require("~root/actions/organizations/createOrganizationUser"); +const postOrganizationSchema = require("./schemas/postOrganizationSchema"); + +const postOrganization = async (req, res) => { + const { userId } = req.user; + const { organizationName } = req.body; + + try { + await postOrganizationSchema.validate( + { + userId, + organizationName + }, + { + abortEarly: false + } + ); + + const { organizationId } = await createOrganization({ organizationName }); + + await createOrganizationUser({ + userId, + userOrganizationRoleId: 1, + organizationId, + addedBy: userId + }); + + res.status(201).send({ + organizationId, + organizationAdmin: userId + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = postOrganization; diff --git a/api-sample/src/app/controllers/organizations/postOrganization/schemas/postOrganizationSchema.js b/api-sample/src/app/controllers/organizations/postOrganization/schemas/postOrganizationSchema.js new file mode 100644 index 00000000..507937f2 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/postOrganization/schemas/postOrganizationSchema.js @@ -0,0 +1,28 @@ +const yup = require("yup"); +const selectUser = require("./queries/selectuser"); + +const postOrganizationSchema = yup.object().shape({ + userId: yup + .number() + .required("The user Id is required") + .label("User Id") + .typeError("The User Id must be a valid number") + .test("DoesUserExist", "The User does not exist", async function test( + userId + ) { + const isUser = await selectUser({ + userId + }); + if (isUser) { + return true; + } + + return false; + }), + organizationName: yup + .string() + .required("The Organization Name is a required field") + .label("Name") + .typeError("Name must be a string.") +}); +module.exports = postOrganizationSchema; diff --git a/api-sample/src/app/controllers/organizations/postOrganization/schemas/queries/selectuser.js b/api-sample/src/app/controllers/organizations/postOrganization/schemas/queries/selectuser.js new file mode 100644 index 00000000..385174ec --- /dev/null +++ b/api-sample/src/app/controllers/organizations/postOrganization/schemas/queries/selectuser.js @@ -0,0 +1,13 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUser = ({ userId }) => submitQuery` + SELECT + user_id, + first_name, + last_name, + email + FROM users + WHERE user_id = ${userId} +`; + +module.exports = getFirst(selectUser, "email"); diff --git a/api-sample/src/app/controllers/organizations/postOrganizationUser/index.js b/api-sample/src/app/controllers/organizations/postOrganizationUser/index.js new file mode 100644 index 00000000..2eb46454 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/postOrganizationUser/index.js @@ -0,0 +1,40 @@ +const createOrganizationUser = require("~root/actions/organizations/createOrganizationUser"); +const fetchUserIdByEmail = require("~root/actions/users/fetchUserByEmail"); +const handleAPIError = require("~root/utils/handleAPIError"); +const postOrganizationUserSchema = require("./schemas/postOrganizationUserSchema"); + +const postOrganizationUser = async (req, res) => { + const { userId } = req.user; + const { email, userRoleId, orgId } = req.body; + + try { + const { newOrgUserUserId } = await fetchUserIdByEmail({ email }); + + await postOrganizationUserSchema.validate( + { + orgId, + userId, + userRoleId, + newOrgUserUserId + }, + { + abortEarly: false + } + ); + + const { organizationUserId } = await createOrganizationUser({ + userId: newOrgUserUserId, + userOrganizationRoleId: userRoleId, + organizationId: orgId, + addedBy: userId + }); + + res.send({ + organizationUserId + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = postOrganizationUser; diff --git a/api-sample/src/app/controllers/organizations/postOrganizationUser/schemas/postOrganizationUserSchema.js b/api-sample/src/app/controllers/organizations/postOrganizationUser/schemas/postOrganizationUserSchema.js new file mode 100644 index 00000000..d5eddfb6 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/postOrganizationUser/schemas/postOrganizationUserSchema.js @@ -0,0 +1,86 @@ +const yup = require("yup"); +const selectSuperAdmin = require("~root/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin"); +const selectOrganizationUser = require("~root/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser"); +const selectUserRoleIfInUserRoles = require("./queries/selectUserRoleIfInUserRoles"); +const selectOrgUserByUserId = require("./queries/selectOrgUserByUserId"); + +const postOrganizationUserSchema = yup.object().shape({ + newOrgUserUserId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("New organization user user_id") + .typeError("The new organization user user_id must be a valid number") + .test( + "doesOrganizationUserExist", + "User should exist and not belong to the given organization.", + async function test(newOrgUserUserId) { + const { orgId } = this.parent; + + const user = await selectOrgUserByUserId({ + newOrgUserUserId, + orgId + }); + if (user) { + return false; + } + + return true; + } + ), + + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test( + "doesUserBelongToOrganization", + "The user must belong to the Organization and have the appropriate access (organization admin or event manager) or be a superAdmin", + async function test(userId) { + const { orgId } = this.parent; + + const isUserSuperAdmin = await selectSuperAdmin({ + userId + }); + if (isUserSuperAdmin) { + return true; + } + + const user = await selectOrganizationUser({ + orgId, + userId + }); + if (user) { + return true; + } + + return false; + } + ), + userRoleId: yup + .number() + .positive() + .min(1, "This field can noy be empty!") + .label("user role id") + .typeError("The user role id must be a valid number") + .test( + "UserRoleIdbelongToUserRoles", + "The user role id should be in the user roles table.", + async function test(userRoleId) { + const userRole = await selectUserRoleIfInUserRoles({ userRoleId }); + if (userRole) { + return true; + } + return false; + } + ), + orgId: yup + .number() + .required() + .label("Organization ID") + .typeError("Organization Id must be a number") +}); + +module.exports = postOrganizationUserSchema; diff --git a/api-sample/src/app/controllers/organizations/postOrganizationUser/schemas/queries/selectOrgUserByUserId.js b/api-sample/src/app/controllers/organizations/postOrganizationUser/schemas/queries/selectOrgUserByUserId.js new file mode 100644 index 00000000..691cf5ac --- /dev/null +++ b/api-sample/src/app/controllers/organizations/postOrganizationUser/schemas/queries/selectOrgUserByUserId.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectOrgUserByUserId = ({ newOrgUserUserId, orgId }) => submitQuery` + SELECT + user_organization_id + FROM user_organizations + WHERE user_id = ${newOrgUserUserId} && organization_id = ${orgId} +`; + +module.exports = getFirst(selectOrgUserByUserId, "user_organization_id"); diff --git a/api-sample/src/app/controllers/organizations/postOrganizationUser/schemas/queries/selectUserRoleIfInUserRoles.js b/api-sample/src/app/controllers/organizations/postOrganizationUser/schemas/queries/selectUserRoleIfInUserRoles.js new file mode 100644 index 00000000..e1e2c802 --- /dev/null +++ b/api-sample/src/app/controllers/organizations/postOrganizationUser/schemas/queries/selectUserRoleIfInUserRoles.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserRoleIfInUserRoles = ({ userRoleId }) => submitQuery` + SELECT + user_role_id + FROM user_roles + WHERE user_role_id = ${userRoleId} +`; + +module.exports = getFirst(selectUserRoleIfInUserRoles, "user_role_id"); diff --git a/api-sample/src/app/controllers/password-recovery/postRecoveryRequest/index.js b/api-sample/src/app/controllers/password-recovery/postRecoveryRequest/index.js index b7b5ace9..2ad6eff7 100644 --- a/api-sample/src/app/controllers/password-recovery/postRecoveryRequest/index.js +++ b/api-sample/src/app/controllers/password-recovery/postRecoveryRequest/index.js @@ -1,8 +1,8 @@ const { v4: uuidv4 } = require("uuid"); const createRecoveryRequest = require("~root/actions/password-recovery/createRecoveryRequest"); const handleApiError = require("~root/utils/handleAPIError"); +const sendEmail = require("~root/services/emails/sendEmail"); const postRecoverRequestSchema = require("./schemas/postRecoveryRequestSchema"); -const sendEmail = require("~root/lib/services/emails/sendEmail"); const postRecoveryRequest = async (req, res) => { const { requestedEmail } = req.body; diff --git a/api-sample/src/app/controllers/userOrganizationInvitations/deleteUserOrganizationInvitations/index.js b/api-sample/src/app/controllers/userOrganizationInvitations/deleteUserOrganizationInvitations/index.js new file mode 100644 index 00000000..665b1993 --- /dev/null +++ b/api-sample/src/app/controllers/userOrganizationInvitations/deleteUserOrganizationInvitations/index.js @@ -0,0 +1,24 @@ +const handleAPIError = require("~root/utils/handleAPIError"); +const removeUserOrganizationInvitations = require("~root/actions/userOrganizationInvitations/removeUserOrganizationInvitations"); +const newDeleteUserOrganizationInvitationsSchema = require("./schema/newDeleteUserOrganizationInvitationsSchema"); + +const deleteUserOrganizationInvitations = async (req, res) => { + const { userOrganizationInvitationId, orgId } = req.params; + + try { + await newDeleteUserOrganizationInvitationsSchema.validate( + { userOrganizationInvitationId }, + { abortEarly: false } + ); + await removeUserOrganizationInvitations({ + userOrganizationInvitationId, + orgId + }); + + res.status(204).send({ success: true }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = deleteUserOrganizationInvitations; diff --git a/api-sample/src/app/controllers/userOrganizationInvitations/deleteUserOrganizationInvitations/schema/newDeleteUserOrganizationInvitationsSchema.js b/api-sample/src/app/controllers/userOrganizationInvitations/deleteUserOrganizationInvitations/schema/newDeleteUserOrganizationInvitationsSchema.js new file mode 100644 index 00000000..3bf64497 --- /dev/null +++ b/api-sample/src/app/controllers/userOrganizationInvitations/deleteUserOrganizationInvitations/schema/newDeleteUserOrganizationInvitationsSchema.js @@ -0,0 +1,30 @@ +const yup = require("yup"); +const selectUserOrganizationInvitationsById = require("./queries/selectUserOrganizationInvitationsById"); + +const newDeleteUserOrganizationInvitationsSchema = yup.object().shape({ + userOrganizationInvitationId: yup + .number() + .integer() + .required() + .label("userOrganizationInvitationId") + .typeError("The userOrganizationInvitationId field must be a number") + .test( + "doesExist", + "The userOrganizationInvitationId field must be a valid userOrganizationInvitationId", + async function test(userOrganizationInvitationId) { + if (!userOrganizationInvitationId) { + return false; + } + + const userOrganizationInvitationIdCount = await selectUserOrganizationInvitationsById( + { userOrganizationInvitationId } + ); + if (userOrganizationInvitationIdCount === 0) { + return false; + } + return true; + } + ) +}); + +module.exports = newDeleteUserOrganizationInvitationsSchema; diff --git a/api-sample/src/app/controllers/userOrganizationInvitations/deleteUserOrganizationInvitations/schema/queries/selectUserOrganizationInvitationsById.js b/api-sample/src/app/controllers/userOrganizationInvitations/deleteUserOrganizationInvitations/schema/queries/selectUserOrganizationInvitationsById.js new file mode 100644 index 00000000..e403fc99 --- /dev/null +++ b/api-sample/src/app/controllers/userOrganizationInvitations/deleteUserOrganizationInvitations/schema/queries/selectUserOrganizationInvitationsById.js @@ -0,0 +1,14 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserOrganizationInvitationsById = ({ + userOrganizationInvitationId +}) => submitQuery` + SELECT + COUNT(*) AS count + FROM + user_organization_invitations + WHERE + user_organization_invitation_id = ${userOrganizationInvitationId} +`; + +module.exports = getFirst(selectUserOrganizationInvitationsById, "count"); diff --git a/api-sample/src/app/controllers/userOrganizationInvitations/getUserOrganizationInvitations/index.js b/api-sample/src/app/controllers/userOrganizationInvitations/getUserOrganizationInvitations/index.js new file mode 100644 index 00000000..33835e0d --- /dev/null +++ b/api-sample/src/app/controllers/userOrganizationInvitations/getUserOrganizationInvitations/index.js @@ -0,0 +1,68 @@ +const { sql } = require("~root/lib/database"); +const { paginate, FILTERS } = require("~root/lib/paginate"); + +const handleAPIError = require("~root/utils/handleAPIError"); +const getUserOrganizationInvitationsSchema = require("./schema/getUserOrganizationInvitationsSchema"); + +const getUserOrganizationInvitations = async (req, res) => { + const { userId } = req.user; + const { orgId } = req.params; + try { + await getUserOrganizationInvitationsSchema.validate( + { orgId, userId }, + { abortEarly: false } + ); + const resultset = await paginate({ + basePath: req.path, + baseTable: "user_organization_invitations", + selectFields: [ + "user_organization_invitations.user_organization_invitation_id", + "user_organization_invitations.accepted_at", + "user_organization_invitations.comment", + "user_organization_invitations.email", + "user_organization_invitations.invitation_shortcode", + "user_organization_invitations.invited_by", + "user_organization_invitations.organization_id", + "user_organization_invitations.sent_at", + "user_organization_invitations.updated_at", + "user_organization_invitations.user_role_id", + sql`users.first_name as invited_by_first_name`, + sql`users.last_name as invited_by_last_name` + ], + joinStatements: [ + sql`LEFT JOIN users ON user_organization_invitations.invited_by = users.user_id` + ], + sortableAttributes: ["user_organization_invitations.sent_at"], + filterableAttributes: [ + { + column: + "user_organization_invitations.user_organization_invitation_id", + operations: [FILTERS.equals] + }, + { + column: "user_organization_invitations.user_role_id", + operations: [FILTERS.equals] + }, + { + column: "user_organization_invitations.email", + operations: [FILTERS.containsIgnoreCase] + } + ], + mandatoryFilter: sql`AND user_organization_invitations.organization_id=${orgId}`, + groupBy: [ + "user_organization_invitations.user_organization_invitation_id" + ], + sortBy: req.query.sort_by, + limit: req.query.page_size, + page: req.query.page, // "first" | "last" | null + direction: req.query.direction, // next | previous + filters: req.query.filters, + cursorValues: req.query.cursor + }); + return res.send(resultset); + } catch (err) { + return handleAPIError(res, err); + } +}; + +module.exports = getUserOrganizationInvitations; diff --git a/api-sample/src/app/controllers/userOrganizationInvitations/getUserOrganizationInvitations/schema/getUserOrganizationInvitationsSchema.js b/api-sample/src/app/controllers/userOrganizationInvitations/getUserOrganizationInvitations/schema/getUserOrganizationInvitationsSchema.js new file mode 100644 index 00000000..fd878037 --- /dev/null +++ b/api-sample/src/app/controllers/userOrganizationInvitations/getUserOrganizationInvitations/schema/getUserOrganizationInvitationsSchema.js @@ -0,0 +1,48 @@ +const yup = require("yup"); +const selectEventUser = require("~root/actions/schemaHelpers/queries/selectEventUser/selectEventUser"); +const selectOrganizationUser = require("~root/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser"); +const selectSuperAdmin = require("~root/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin"); + +const getUserOrganizationInvitationsSchema = yup.object().shape({ + orgId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("Org Id") + .typeError("The org id must be a valid number"), + + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test( + "doesUserBelongToOrganization", + "The user must belong to the Organization and have the appropriate access.", + async function test(userId) { + const { orgId } = this.parent; + + const user = await selectOrganizationUser({ + orgId, + userId + }); + if (user) { + return true; + } + const isUserSuperAdmin = await selectSuperAdmin({ + userId + }); + if (isUserSuperAdmin) { + return true; + } + const eventStaffOrRep = await selectEventUser({ orgId, userId }); + if (eventStaffOrRep) { + return true; + } + return false; + } + ) +}); + +module.exports = getUserOrganizationInvitationsSchema; diff --git a/api-sample/src/app/controllers/userOrganizations/getAllUsersByOrgId/index.js b/api-sample/src/app/controllers/userOrganizations/getAllUsersByOrgId/index.js new file mode 100644 index 00000000..60b8e47c --- /dev/null +++ b/api-sample/src/app/controllers/userOrganizations/getAllUsersByOrgId/index.js @@ -0,0 +1,103 @@ +const { sql } = require("~root/lib/database"); +const { paginate, FILTERS } = require("~root/lib/paginate"); +const handleAPIError = require("~root/utils/handleAPIError"); +const getAllUsersByOrgIdSchema = require("./schemas/getAllUsersByOrgIdSchema"); + +const getAllUsersByOrgId = async (req, res) => { + const { userId } = req.user; + const { orgId } = req.params; + try { + await getAllUsersByOrgIdSchema.validate( + { userId, orgId }, + { abortEarly: false } + ); + const resultset = await paginate({ + basePath: req.path, + baseTable: "user_organizations", + selectFields: [ + "user_organizations.user_organization_id", + "users.first_name", + "users.last_name", + "users.email", + "user_organizations.organization_id", + "users.current_situation", + "users.is_employed", + "user_organizations.user_role_id", + "user_organizations.created_at", + "user_organizations.disabled_at", + "user_organizations.disabled_by", + "user_organizations.updated_at", + "user_organizations.user_id", + "user_organizations.added_by", + sql`u1.first_name as added_by_firstname`, + sql`u1.last_name as added_by_lastname` + ], + joinStatements: [ + sql`LEFT JOIN user_roles ON user_organizations.user_role_id = user_roles.user_role_id`, + sql`LEFT JOIN users ON user_organizations.user_id = users.user_id`, + sql`LEFT JOIN users u1 ON u1.user_id = user_organizations.added_by` + ], + sortableAttributes: ["user_organizations.user_organization_id"], + groupBy: ["user_organizations.user_organization_id"], + filterableAttributes: [ + { + column: "user_organizations.user_role_id", + operations: [FILTERS.equals] + }, + { + column: "users.user_id", + operations: [ + FILTERS.containsIgnoreCase, + { + operator: "searchOrgUsersByFullName", + description: "search org users by fullname", + minimumNumberOfOperands: 1, + maximumNumberOfOperands: 1, + filterFn: operands => { + const filterString = operands[0]; + if (filterString) { + return sql`(LOWER(users.first_name) LIKE ${`%${filterString}%`} OR LOWER(users.last_name) LIKE ${`%${filterString}%`}) `; + } + return sql``; + } + }, + { + operator: "showInactiveOnly", + description: + "show users who are not assigned for particular event", + minimumNumberOfOperands: 1, + maximumNumberOfOperands: 999, + filterFn: operands => { + const eventId = operands[0]; + if (eventId) { + return sql`users.user_id NOT IN ( + select user_id from event_staff + inner join user_organizations ON event_staff.user_organization_id = user_organizations.user_organization_id + where event_staff.event_id = ${eventId})`; + } + + return sql``; + } + } + ] + }, + { + column: "user_organizations.organization_id", + operations: [FILTERS.equals] + } + ], + mandatoryFilter: sql`AND user_organizations.organization_id=${orgId}`, + sortBy: req.query.sort_by, + limit: req.query.page_size, + page: req.query.page, // "first" | "last" | null + direction: req.query.direction, // next | previous + filters: req.query.filters, + cursorValues: req.query.cursor + }); + return res.send(resultset); + } catch (err) { + return handleAPIError(res, err); + } +}; + +module.exports = getAllUsersByOrgId; diff --git a/api-sample/src/app/controllers/userOrganizations/getAllUsersByOrgId/schemas/getAllUsersByOrgIdSchema.js b/api-sample/src/app/controllers/userOrganizations/getAllUsersByOrgId/schemas/getAllUsersByOrgIdSchema.js new file mode 100644 index 00000000..57997ae5 --- /dev/null +++ b/api-sample/src/app/controllers/userOrganizations/getAllUsersByOrgId/schemas/getAllUsersByOrgIdSchema.js @@ -0,0 +1,48 @@ +const yup = require("yup"); +const selectEventUser = require("~root/actions/schemaHelpers/queries/selectEventUser/selectEventUser"); +const selectOrganizationUser = require("~root/actions/schemaHelpers/queries/selectOrganizationUser/selectOrganizationUser"); +const selectSuperAdmin = require("~root/actions/schemaHelpers/queries/selectSuperAdmin/selectSuperAdmin"); + +const getAllUsersByOrgIdSchema = yup.object().shape({ + orgId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("Org Id") + .typeError("The org id must be a valid number"), + + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test( + "doesUserBelongToOrganization", + "The user must belong to the Organization and have the appropriate access.", + async function test(userId) { + const { orgId } = this.parent; + + const user = await selectOrganizationUser({ + orgId, + userId + }); + if (user) { + return true; + } + const isUserSuperAdmin = await selectSuperAdmin({ + userId + }); + if (isUserSuperAdmin) { + return true; + } + const eventStaffOrRep = await selectEventUser({ orgId, userId }); + if (eventStaffOrRep) { + return true; + } + return false; + } + ) +}); + +module.exports = getAllUsersByOrgIdSchema; diff --git a/api-sample/src/app/controllers/users/getPasswordRecoveryRequestByShortcode/index.js b/api-sample/src/app/controllers/users/getPasswordRecoveryRequestByShortcode/index.js new file mode 100644 index 00000000..83d80ac3 --- /dev/null +++ b/api-sample/src/app/controllers/users/getPasswordRecoveryRequestByShortcode/index.js @@ -0,0 +1,24 @@ +const fetchRequestByShortcode = require("~root/actions/users/fetchRequestByShortcode"); +const handleAPIError = require("~root/utils/handleAPIError"); + +const getPasswordRecoveryRequestByShortcode = async (req, res) => { + const { shortcode } = req.params; + try { + await getRequestByShortcode.validate( + { + shortcode + }, + { + abortEarly: false + } + ); + const { request } = await fetchRequestByShortcode({ shortcode }); + res.status(201).send({ + request + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = getPasswordRecoveryRequestByShortcode; diff --git a/api-sample/src/app/controllers/users/getPasswordRecoveryRequestByShortcode/schemas/getRequestByShortcodeSchema.js b/api-sample/src/app/controllers/users/getPasswordRecoveryRequestByShortcode/schemas/getRequestByShortcodeSchema.js new file mode 100644 index 00000000..4564c581 --- /dev/null +++ b/api-sample/src/app/controllers/users/getPasswordRecoveryRequestByShortcode/schemas/getRequestByShortcodeSchema.js @@ -0,0 +1,19 @@ +const yup = require("yup"); +const selectRequestByShortcode = require("./queries/selectRequestByShortcode"); + +const getRequestByShortcode = yup.object().shape({ + shortcode: yup + .string() + .label("Shortcode") + .typeError("The shortcode must be valid.") + .test("RequestMustExist", "The request doesnot exist", async function test( + shortcode + ) { + const isShortcodeExit = await selectRequestByShortcode({ shortcode }); + if (isShortcodeExit) { + return true; + } + return false; + }) +}); +module.exports = getRequestByShortcode; diff --git a/api-sample/src/app/controllers/users/getPasswordRecoveryRequestByShortcode/schemas/queries/selectRequestByShortcode.js b/api-sample/src/app/controllers/users/getPasswordRecoveryRequestByShortcode/schemas/queries/selectRequestByShortcode.js new file mode 100644 index 00000000..cc4b04fc --- /dev/null +++ b/api-sample/src/app/controllers/users/getPasswordRecoveryRequestByShortcode/schemas/queries/selectRequestByShortcode.js @@ -0,0 +1,18 @@ +const { submitQuery, camelKeys, getFirst } = require("~root/lib/database"); + +const selectRequestByShortcode = ({ shortcode }) => submitQuery` + SELECT + password_recovery_request_id, + email, + shortcode, + recovered_at, + expiry_date, + created_at + FROM password_recovery_requests + WHERE shortcode = ${shortcode} +`; + +module.exports = getFirst( + camelKeys(selectRequestByShortcode), + "passwordRecoveryRequestId" +); diff --git a/api-sample/src/app/controllers/users/getRegisterWithInvitation/index.js b/api-sample/src/app/controllers/users/getRegisterWithInvitation/index.js new file mode 100644 index 00000000..8f224c11 --- /dev/null +++ b/api-sample/src/app/controllers/users/getRegisterWithInvitation/index.js @@ -0,0 +1,37 @@ +const handleAPIError = require("~root/utils/handleAPIError"); +const fetchInvitationByShortCode = require("~root/actions/invitations/fetchInvitationByShortCode"); +const fetchOrganizationNameById = require("~root/actions/organizations/fetchOrganizationNameById"); +const postRegisterWithInvitationSchema = require("./schema/postRegisterWithInvitationSchema"); + +const getRegisterWithInvitation = async (req, res) => { + const { invitationShortcode } = req.params; + + try { + await postRegisterWithInvitationSchema.validate( + { + invitationShortcode + }, + { + abortEarly: false + } + ); + + const { invitation } = await fetchInvitationByShortCode({ + invitationShortcode + }); + const { email, organizationId } = invitation; + + const { organizationName } = await fetchOrganizationNameById({ + organizationId + }); + + res.status(201).send({ + email, + organizationName + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = getRegisterWithInvitation; diff --git a/api-sample/src/app/controllers/users/getRegisterWithInvitation/schema/postRegisterWithInvitationSchema.js b/api-sample/src/app/controllers/users/getRegisterWithInvitation/schema/postRegisterWithInvitationSchema.js new file mode 100644 index 00000000..15976dc8 --- /dev/null +++ b/api-sample/src/app/controllers/users/getRegisterWithInvitation/schema/postRegisterWithInvitationSchema.js @@ -0,0 +1,29 @@ +const yup = require("yup"); +const selectInvitationByShortCode = require("./queries/selectInvitationByShortCode"); + +const postRegisterWithInvitationSchema = yup.object().shape({ + invitationShortcode: yup + .string() + .min(1, "This field can not be empty!") + .max(150, "This field has to be less than 150 characters") + .required() + .label("Invitation Shortcode") + .typeError("Not a valid Invitation Shortcode") + .test( + "ShortCodeMustExist", + "The Invitation ShortCode must belong to an email", + async function test(invitationShortcode) { + const { email } = this.parent; + const invitation = await selectInvitationByShortCode({ + invitationShortcode, + email + }); + if (invitation) { + return true; + } + return false; + } + ) +}); + +module.exports = postRegisterWithInvitationSchema; diff --git a/api-sample/src/app/controllers/users/getRegisterWithInvitation/schema/queries/selectInvitationByShortCode.js b/api-sample/src/app/controllers/users/getRegisterWithInvitation/schema/queries/selectInvitationByShortCode.js new file mode 100644 index 00000000..19ddd277 --- /dev/null +++ b/api-sample/src/app/controllers/users/getRegisterWithInvitation/schema/queries/selectInvitationByShortCode.js @@ -0,0 +1,13 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectInvitationByShortCode = ({ + invitationShortcode, + email +}) => submitQuery` + SELECT + email + FROM user_organization_invitations + WHERE invitation_shortcode = ${invitationShortcode} && email = ${email} +`; + +module.exports = getFirst(selectInvitationByShortCode); diff --git a/api-sample/src/app/controllers/users/getUserOrganizations/index.js b/api-sample/src/app/controllers/users/getUserOrganizations/index.js new file mode 100644 index 00000000..c901cb14 --- /dev/null +++ b/api-sample/src/app/controllers/users/getUserOrganizations/index.js @@ -0,0 +1,25 @@ +const handleAPIError = require("~root/utils/handleAPIError"); +const fetchUserOrganizations = require("~root/actions/users/fetchUserOrganizations"); +const getUserOrganizationsSchema = require("./schemas/getUserOrganizationsSchema"); + +const getUserOrganizations = async (req, res) => { + const { userId } = req.user; + try { + await getUserOrganizationsSchema.validate( + { + userId + }, + { + abortEarly: false + } + ); + const { organizations } = await fetchUserOrganizations({ userId }); + res.status(201).send({ + organizations + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = getUserOrganizations; diff --git a/api-sample/src/app/controllers/users/getUserOrganizations/schemas/getUserOrganizationsSchema.js b/api-sample/src/app/controllers/users/getUserOrganizations/schemas/getUserOrganizationsSchema.js new file mode 100644 index 00000000..e4f4c792 --- /dev/null +++ b/api-sample/src/app/controllers/users/getUserOrganizations/schemas/getUserOrganizationsSchema.js @@ -0,0 +1,22 @@ +const yup = require("yup"); +const selectUserById = require("./queries/selectUserById"); + +const getUserOrganizationsSchema = yup.object().shape({ + userId: yup + .number() + .label("userId") + .typeError("userId is invalid.") + .test( + "doesUserExists", + "User account does not exists.", + async function test(userId) { + const user = await selectUserById({ userId }); + if (user) { + return true; + } + return false; + } + ) +}); + +module.exports = getUserOrganizationsSchema; diff --git a/api-sample/src/app/controllers/users/getUserOrganizations/schemas/queries/selectUserById.js b/api-sample/src/app/controllers/users/getUserOrganizations/schemas/queries/selectUserById.js new file mode 100644 index 00000000..78f5a09a --- /dev/null +++ b/api-sample/src/app/controllers/users/getUserOrganizations/schemas/queries/selectUserById.js @@ -0,0 +1,10 @@ +const { submitQuery, camelKeys, getFirst } = require("~root/lib/database"); + +const selectUserById = ({ userId }) => submitQuery` + SELECT + user_id + FROM users + WHERE user_id=${userId}; +`; + +module.exports = getFirst(camelKeys(selectUserById), "userId"); diff --git a/api-sample/src/app/controllers/users/patchUserInvitation/index.js b/api-sample/src/app/controllers/users/patchUserInvitation/index.js new file mode 100644 index 00000000..64f9eb67 --- /dev/null +++ b/api-sample/src/app/controllers/users/patchUserInvitation/index.js @@ -0,0 +1,50 @@ +const createOrganizationUser = require("~root/actions/organizations/createOrganizationUser"); +const fetchUserInvitation = require("~root/actions/users/fetchUserInvitation"); +const modifyUserInvitation = require("~root/actions/users/modifyUserInvitation"); +const handleAPIError = require("~root/utils/handleAPIError"); +const patchUserInvitationSchema = require("./schemas/patchUserInvitationSchema"); + +const patchUserInvitation = async (req, res) => { + const { userId, email } = req.user; + const { invitationId, shortcode } = req.body; + try { + await patchUserInvitationSchema.validate( + { + userId, + invitationId, + shortcode, + email + }, + { + abortEarly: false + } + ); + + const { invitation } = await fetchUserInvitation({ + email, + invitationId, + shortcode + }); + const { userOrganizationRoleId, organizationId, invitedBy } = invitation; + + const { patchedInvitationId } = await modifyUserInvitation({ + invitationId, + shortcode + }); + const { organizationUserId } = await createOrganizationUser({ + userId, + userOrganizationRoleId, + organizationId, + addedBy: invitedBy + }); + + res.status(201).send({ + patchedInvitationId, + organizationUserId + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = patchUserInvitation; diff --git a/api-sample/src/app/controllers/users/patchUserInvitation/schemas/patchUserInvitationSchema.js b/api-sample/src/app/controllers/users/patchUserInvitation/schemas/patchUserInvitationSchema.js new file mode 100644 index 00000000..1a103249 --- /dev/null +++ b/api-sample/src/app/controllers/users/patchUserInvitation/schemas/patchUserInvitationSchema.js @@ -0,0 +1,60 @@ +const yup = require("yup"); +const selectUserById = require("./queries/selectUserById"); +const selectUserByInvitationId = require("./queries/selectUserByInvitationId"); + +const patchUserInvitationSchema = yup.object().shape({ + email: yup + .string() + .email() + .label("Email") + .typeError("Email is invalid."), + userId: yup + .number() + .positive() + .min(1, "This field can not be empty!") + .label("User Id") + .typeError("The user id must be a valid number") + .test( + "doesUserBelongToOrganization", + "The user must belong to the Organization and have the appropriate access (organization admin or event manager) or be a superAdmin", + async function test(userId) { + const { email } = this.parent; + const userID = await selectUserById({ + userId, + email + }); + if (userID) { + return true; + } + + return false; + } + ), + invitationId: yup + .number() + .positive() + .label("Invitation Id") + .typeError("The invitation id must be valid number.") + .test( + "doesInvitationIdExist", + "The invitation must be exist in the organization", + async function test(invitationId) { + const { shortcode } = this.parent; + const hasInvitationId = await selectUserByInvitationId({ + invitationId, + shortcode + }); + if (hasInvitationId) { + return true; + } + + return false; + } + ), + shortcode: yup + .string() + .label("Shortcode") + .typeError("The shortcode must be string.") +}); + +module.exports = patchUserInvitationSchema; diff --git a/api-sample/src/app/controllers/users/patchUserInvitation/schemas/queries/selectUserById.js b/api-sample/src/app/controllers/users/patchUserInvitation/schemas/queries/selectUserById.js new file mode 100644 index 00000000..37238fb9 --- /dev/null +++ b/api-sample/src/app/controllers/users/patchUserInvitation/schemas/queries/selectUserById.js @@ -0,0 +1,11 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserById = ({ userId, email }) => submitQuery` + SELECT + first_name, + email + FROM users + WHERE user_id = ${userId} && email=${email} +`; + +module.exports = getFirst(selectUserById); diff --git a/api-sample/src/app/controllers/users/patchUserInvitation/schemas/queries/selectUserByInvitationId.js b/api-sample/src/app/controllers/users/patchUserInvitation/schemas/queries/selectUserByInvitationId.js new file mode 100644 index 00000000..06d18e4e --- /dev/null +++ b/api-sample/src/app/controllers/users/patchUserInvitation/schemas/queries/selectUserByInvitationId.js @@ -0,0 +1,12 @@ +const { submitQuery, getFirst, camelKeys } = require("~root/lib/database"); + +const selectUserByInvitationId = ({ invitationId, shortcode }) => submitQuery` + SELECT + invitation_shortcode, + email, + user_organization_invitation_id + FROM user_organization_invitations + WHERE user_organization_invitation_id = ${invitationId} && invitation_shortcode = ${shortcode} +`; + +module.exports = getFirst(camelKeys(selectUserByInvitationId)); diff --git a/api-sample/src/app/controllers/users/postCompleteRegistration/index.js b/api-sample/src/app/controllers/users/postCompleteRegistration/index.js new file mode 100644 index 00000000..9efae140 --- /dev/null +++ b/api-sample/src/app/controllers/users/postCompleteRegistration/index.js @@ -0,0 +1,62 @@ +const jwt = require("jsonwebtoken"); +const createCompleteRegistration = require("~root/actions/users/createCompleteRegistration"); +const handleApiError = require("~root/utils/handleAPIError"); +const fetchRegistrationRequestByShortcode = require("~root/actions/users/fetchRegistrationRequestByShortcode"); +const modifyRegistrationRequestStatus = require("~root/actions/users/modifyRegistrationRequestStatus"); +const createOrganizationUser = require("~root/actions/organizations/createOrganizationUser"); +const createOrganization = require("~root/actions/organizations/createOrganization"); + +const postCompleteRegistration = async (req, res) => { + const { registrationShortcode } = req.params; + + try { + const { potentialNewUserData } = await fetchRegistrationRequestByShortcode({ + registrationShortcode + }); + + const { + firstName, + lastName, + email, + password, + organizationName + } = potentialNewUserData; + + const { userId } = await createCompleteRegistration({ + firstName, + lastName, + email, + password + }); + + const { organizationId } = await createOrganization({ + organizationName + }); + + await createOrganizationUser({ + userId, + userOrganizationRoleId: 1, + organizationId, + addedBy: userId + }); + + await modifyRegistrationRequestStatus({ registrationShortcode }); + + const accessToken = jwt.sign( + { ...potentialNewUserData, userId }, + process.env.JWT_SECRET, + { + expiresIn: "2h" // 2 Hours + } + ); + + res.send({ + userId, + accessToken + }); + } catch (err) { + handleApiError(res, err); + } +}; + +module.exports = postCompleteRegistration; diff --git a/api-sample/src/app/controllers/users/postRegisterWithInvitation/index.js b/api-sample/src/app/controllers/users/postRegisterWithInvitation/index.js new file mode 100644 index 00000000..fa7f2f19 --- /dev/null +++ b/api-sample/src/app/controllers/users/postRegisterWithInvitation/index.js @@ -0,0 +1,81 @@ +const axios = require("axios"); +const handleAPIError = require("~root/utils/handleAPIError"); +const fetchInvitationByShortCode = require("~root/actions/invitations/fetchInvitationByShortCode"); +const createUser = require("~root/actions/users/createUser"); +const createOrganizationUser = require("~root/actions/organizations/createOrganizationUser"); +const postRegisterWithInvitationSchema = require("./schema/postRegisterWithInvitationSchema"); + +// eslint-disable-next-line consistent-return +const postRegisterWithInvitation = async (req, res) => { + const { firstName, lastName, password, captcha } = req.body; + + const { invitationShortcode } = req.params; + + try { + await postRegisterWithInvitationSchema.validate( + { + firstName, + lastName, + password, + invitationShortcode + }, + { + abortEarly: false + } + ); + + if (!captcha) { + return res.status(400).json({ error: "captcha is required" }); + } + + const secretKey = process.env.RECAPTCHA_SECRET_KEY; + + const verificationParams = { + secret: secretKey, + response: captcha + }; + + const verificationUrl = `https://www.google.com/recaptcha/api/siteverify?secret=${secretKey}&response=${captcha}`; + + const { data } = await axios.post(verificationUrl, verificationParams); + + if (!data.success) { + return res.status(500).send({ error: "Captcha verification failed" }); + } + + const { invitation } = await fetchInvitationByShortCode({ + invitationShortcode + }); + + const { + organizationId, + invitedBy, + userOrganizationRoleId, + email + } = invitation; + + const { user } = await createUser({ + firstName, + lastName, + password, + email, + isSuperAdmin: 0 + }); + + const { organizationUserId } = await createOrganizationUser({ + userId: user, + userOrganizationRoleId, + organizationId, + addedBy: invitedBy + }); + + res.status(201).send({ + user, + organizationUserId + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = postRegisterWithInvitation; diff --git a/api-sample/src/app/controllers/users/postRegisterWithInvitation/schema/postRegisterWithInvitationSchema.js b/api-sample/src/app/controllers/users/postRegisterWithInvitation/schema/postRegisterWithInvitationSchema.js new file mode 100644 index 00000000..f267a1ef --- /dev/null +++ b/api-sample/src/app/controllers/users/postRegisterWithInvitation/schema/postRegisterWithInvitationSchema.js @@ -0,0 +1,57 @@ +const yup = require("yup"); +const selectInvitationByShortCode = require("./queries/selectInvitationByShortCode"); + +const postRegisterWithInvitationSchema = yup.object().shape({ + firstName: yup + .string() + .min(1, "This field can not be empty!") + .max(50, "This field has to be less than 50 characters") + .required() + .label("First Name") + .typeError("Not a valid First Name"), + + lastName: yup + .string() + .min(1, "This field can not be empty!") + .max(50, "This field has to be less than 50 characters") + .required() + .label("Last Name") + .typeError("Not a valid Last Name"), + + password: yup + .string() + .min(8, "Password must be at least 8 characters long!") + .max(500, "This field has to be less than 500 characters") + .matches( + /^(?=.*[A-Z])(?=.*[!@#$&*])(?=.*[0-9])(?=.*[a-z]).{8,}$/, + "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character" + ) + .required() + .label("Password") + .typeError("Not a valid Password"), + + invitationShortcode: yup + .string() + .min(1, "This field can not be empty!") + .max(150, "This field has to be less than 150 characters") + .required() + .label("Invitation Shortcode") + .typeError("Not a valid Invitation Shortcode") + .test( + "ShortCodeMustExist", + "The Invitation ShortCode must belong to an email", + async function test(invitationShortcode) { + const { email } = this.parent; + const invitation = await selectInvitationByShortCode({ + invitationShortcode, + email + }); + if (invitation) { + return true; + } + return false; + } + ) +}); + +module.exports = postRegisterWithInvitationSchema; diff --git a/api-sample/src/app/controllers/users/postRegisterWithInvitation/schema/queries/selectInvitationByShortCode.js b/api-sample/src/app/controllers/users/postRegisterWithInvitation/schema/queries/selectInvitationByShortCode.js new file mode 100644 index 00000000..19ddd277 --- /dev/null +++ b/api-sample/src/app/controllers/users/postRegisterWithInvitation/schema/queries/selectInvitationByShortCode.js @@ -0,0 +1,13 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectInvitationByShortCode = ({ + invitationShortcode, + email +}) => submitQuery` + SELECT + email + FROM user_organization_invitations + WHERE invitation_shortcode = ${invitationShortcode} && email = ${email} +`; + +module.exports = getFirst(selectInvitationByShortCode); diff --git a/api-sample/src/app/controllers/users/postRegistrationRequest/index.js b/api-sample/src/app/controllers/users/postRegistrationRequest/index.js new file mode 100644 index 00000000..c44e1d33 --- /dev/null +++ b/api-sample/src/app/controllers/users/postRegistrationRequest/index.js @@ -0,0 +1,82 @@ +const { v4: uuidv4 } = require("uuid"); +const axios = require("axios"); +const handleAPIError = require("~root/utils/handleAPIError"); +const createRegistrationRequest = require("~root/actions/users/createRegistrationRequest"); +const sendEmail = require("~root/services/emails/sendEmail"); +const postRegistrationRequestSchema = require("./schema/postRegistrationRequestSchema"); + +// eslint-disable-next-line consistent-return +const postRegistrationRequest = async (req, res) => { + const registrationShortcode = uuidv4(); + + const { + firstName, + lastName, + email, + password, + organizationName, + captcha + } = req.body; + + try { + await postRegistrationRequestSchema.validate( + { + firstName, + lastName, + email, + password, + organizationName + }, + { + abortEarly: false + } + ); + + // if (!captcha) { + // return res.status(400).json({ error: "captcha is required" }); + // } + + // const secretKey = process.env.RECAPTCHA_SECRET_KEY; + + // const verificationParams = { + // secret: secretKey, + // response: captcha + // }; + + // const verificationUrl = `https://www.google.com/recaptcha/api/siteverify?secret=${secretKey}&response=${captcha}`; + + // const { data } = await axios.post(verificationUrl, verificationParams); + + // if (!data.success) { + // return res.status(500).send({ error: "Captcha verification failed" }); + // } + + const { createdRegistrationRequest } = await createRegistrationRequest({ + firstName, + lastName, + email, + password, + organizationName, + registrationShortcode + }); + + // const emailPayload = { + // to: email, + // template: "email-verification", + // version: "0.0.1", + // metadata: { + // firstName, + // emailVerification: `${process.env.APP_BASE_URL}/complete-registration/${registrationShortcode}` + // } + // }; + // sendEmail(emailPayload); + + res.status(201).send({ + createdRegistrationRequest + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = postRegistrationRequest; diff --git a/api-sample/src/app/controllers/users/postRegistrationRequest/schema/postRegistrationRequestSchema.js b/api-sample/src/app/controllers/users/postRegistrationRequest/schema/postRegistrationRequestSchema.js new file mode 100644 index 00000000..d3d8ab3e --- /dev/null +++ b/api-sample/src/app/controllers/users/postRegistrationRequest/schema/postRegistrationRequestSchema.js @@ -0,0 +1,67 @@ +const yup = require("yup"); +const selectUserByEmail = require("./queries/selectUserByEmail"); + +const postRegistrationRequestSchema = yup.object().shape({ + firstName: yup + .string() + .min(1, "This field can not be empty!") + .max(50, "This field has to be less than 50 characters") + .required() + .label("First Name") + .typeError("Not a valid First Name"), + + lastName: yup + .string() + .min(1, "This field can not be empty!") + .max(50, "This field has to be less than 50 characters") + .required() + .label("Last Name") + .typeError("Not a valid Last Name"), + + email: yup + .string() + .min(1, "This field can not be empty!") + .max(50, "This field has to be less than 50 characters") + .email() + .required() + .label("Email") + .typeError("Not a valid Email") + .test( + "Does User already exist in platform", + "This email already registered to platform", + + async function test(email) { + const userEmail = await selectUserByEmail({ + email + }); + + if (!userEmail) { + return true; + } + + return false; + } + ), + + password: yup + .string() + .min(8, "Password must be at least 8 characters long!") + .max(500, "This field has to be less than 500 characters") + .matches( + /^(?=.*[A-Z])(?=.*[!@#$&*])(?=.*[0-9])(?=.*[a-z]).{8,}$/, + "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character" + ) + .required() + .label("Password") + .typeError("Not a valid Password"), + + organizationName: yup + .string() + .min(1, "This field can not be empty!") + .max(50, "This field has to be less than 50 characters") + .required() + .label("Company Name") + .typeError("Not a valid Company Name") +}); + +module.exports = postRegistrationRequestSchema; diff --git a/api-sample/src/app/controllers/users/postRegistrationRequest/schema/queries/selectUserByEmail.js b/api-sample/src/app/controllers/users/postRegistrationRequest/schema/queries/selectUserByEmail.js new file mode 100644 index 00000000..264d3e68 --- /dev/null +++ b/api-sample/src/app/controllers/users/postRegistrationRequest/schema/queries/selectUserByEmail.js @@ -0,0 +1,10 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserByEmail = ({ email }) => submitQuery` + SELECT + email + FROM registration_requests + WHERE email IN (${email}); +`; + +module.exports = getFirst(selectUserByEmail, "email"); diff --git a/api-sample/src/app/controllers/users/putUserDetails/index.js b/api-sample/src/app/controllers/users/putUserDetails/index.js index be7bb7d6..98d447d0 100644 --- a/api-sample/src/app/controllers/users/putUserDetails/index.js +++ b/api-sample/src/app/controllers/users/putUserDetails/index.js @@ -5,14 +5,13 @@ const handleAPIError = require("~root/utils/handleAPIError"); const putUserDetails = async (req, res) => { const { userId } = req.user; - const { firstName, lastName, password, jobTitle } = req.body; + const { firstName, lastName, password } = req.body; try { const { userDetails } = await modifyUser({ userId, firstName, lastName, - password, - jobTitle + password }); const { user } = await fetchUserById({ @@ -20,7 +19,7 @@ const putUserDetails = async (req, res) => { }); const accessToken = jwt.sign({ ...user }, process.env.JWT_SECRET, { - expiresIn: "365d" // 1 year + expiresIn: "2h" // 2 Hours }); res.send({ diff --git a/api-sample/src/app/controllers/users/register/index.js b/api-sample/src/app/controllers/users/register/index.js index 02d7fbe3..4f1c60e7 100644 --- a/api-sample/src/app/controllers/users/register/index.js +++ b/api-sample/src/app/controllers/users/register/index.js @@ -3,7 +3,14 @@ const handleAPIError = require("~root/utils/handleAPIError"); const postUserSchema = require("./schemas/postUserSchema"); const postUser = async (req, res) => { - const { firstName, lastName, email, password, userTypeId } = req.body; + const { + firstName, + lastName, + email, + password, + currentSituation, + isEmployed + } = req.body; try { await postUserSchema.validate( @@ -12,23 +19,25 @@ const postUser = async (req, res) => { lastName, email, password, - userTypeId + currentSituation, + isEmployed }, { abortEarly: false } ); - const { user } = await createUser({ + const { userId } = await createUser({ firstName, lastName, email, password, - userTypeId + currentSituation, + isEmployed }); res.status(201).send({ - user + userId }); } catch (err) { handleAPIError(res, err); diff --git a/api-sample/src/app/controllers/users/register/schemas/postUserSchema.js b/api-sample/src/app/controllers/users/register/schemas/postUserSchema.js index 25831f32..dc17ba11 100644 --- a/api-sample/src/app/controllers/users/register/schemas/postUserSchema.js +++ b/api-sample/src/app/controllers/users/register/schemas/postUserSchema.js @@ -7,22 +7,20 @@ const postUserSchema = yup.object().shape({ .required() .label("First Name") .typeError("First Name must be a number."), + lastName: yup .string() .required() .label("Last Name") .typeError("Last Name must be a number."), + password: yup .string() .min(8) .required() .label("password") .typeError("password must be a number."), - userTypeId: yup - .number() - .required() - .label("User Type ID") - .typeError("User Type Id must be a number."), + email: yup .string() .email() @@ -38,6 +36,9 @@ const postUserSchema = yup.object().shape({ } return true; }); - }) + }), + currentSituation: yup.string(), + isEmployed: yup.number() }); + module.exports = postUserSchema; diff --git a/api-sample/src/app/controllers/users/registerAdmin/index.js b/api-sample/src/app/controllers/users/registerAdmin/index.js new file mode 100644 index 00000000..6f7536e3 --- /dev/null +++ b/api-sample/src/app/controllers/users/registerAdmin/index.js @@ -0,0 +1,38 @@ +const createUser = require("~root/actions/users/createUser"); +const handleAPIError = require("~root/utils/handleAPIError"); +const postUserSchema = require("./schemas/postUserSchema"); + +const postUser = async (req, res) => { + const { firstName, lastName, email, password, isSuperAdmin } = req.body; + + try { + await postUserSchema.validate( + { + firstName, + lastName, + email, + password, + isSuperAdmin + }, + { + abortEarly: false + } + ); + + const { user } = await createUser({ + firstName, + lastName, + email, + password, + isSuperAdmin + }); + + res.status(201).send({ + user + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = postUser; diff --git a/api-sample/src/app/controllers/users/registerAdmin/schemas/postUserSchema.js b/api-sample/src/app/controllers/users/registerAdmin/schemas/postUserSchema.js new file mode 100644 index 00000000..c0c445d3 --- /dev/null +++ b/api-sample/src/app/controllers/users/registerAdmin/schemas/postUserSchema.js @@ -0,0 +1,43 @@ +const yup = require("yup"); +const selectUserByEmail = require("./queries/selectUserById"); + +const postUserSchema = yup.object().shape({ + firstName: yup + .string() + .required() + .label("First Name") + .typeError("First Name must be a number."), + lastName: yup + .string() + .required() + .label("Last Name") + .typeError("Last Name must be a number."), + password: yup + .string() + .min(8) + .required() + .label("password") + .typeError("password must be a number."), + email: yup + .string() + .email() + .required() + .label("Email") + .typeError("Email is invalid.") + .test("doesEmailExist", "User account already exists.", function test( + email + ) { + return selectUserByEmail({ email }).then(account => { + if (account) { + return false; + } + return true; + }); + }), + isSuperAdmin: yup + .boolean() + .required() + .label("isSuperAdmin") + .typeError("isSuperAdmin must be a boolean.") +}); +module.exports = postUserSchema; diff --git a/api-sample/src/app/controllers/users/registerAdmin/schemas/queries/selectUserById.js b/api-sample/src/app/controllers/users/registerAdmin/schemas/queries/selectUserById.js new file mode 100644 index 00000000..4dacc07e --- /dev/null +++ b/api-sample/src/app/controllers/users/registerAdmin/schemas/queries/selectUserById.js @@ -0,0 +1,10 @@ +const { submitQuery, camelKeys, getFirst } = require("~root/lib/database"); + +const selectUserByEmail = ({ email }) => submitQuery` + SELECT + user_id + FROM users + WHERE email=${email}; +`; + +module.exports = getFirst(camelKeys(selectUserByEmail)); diff --git a/api-sample/src/app/controllers/users/userTypes/index.js b/api-sample/src/app/controllers/users/userTypes/index.js deleted file mode 100644 index c52ca15b..00000000 --- a/api-sample/src/app/controllers/users/userTypes/index.js +++ /dev/null @@ -1,16 +0,0 @@ -const fetchUserTypes = require("~root/actions/users/fetchUserTypes"); -const handleAPIError = require("~root/utils/handleAPIError"); - -const getUserTypes = async (req, res) => { - try { - const { userTypes } = await fetchUserTypes(); - - res.status(201).send({ - userTypes - }); - } catch (err) { - handleAPIError(res, err); - } -}; - -module.exports = getUserTypes; diff --git a/api-sample/src/app/routes.js b/api-sample/src/app/routes.js index a8a1f01d..8aee5993 100644 --- a/api-sample/src/app/routes.js +++ b/api-sample/src/app/routes.js @@ -1,33 +1,128 @@ const express = require("express"); +const { ADMIN } = require("~root/constants/userTypes"); const postLogin = require("./controllers/users/login"); -const postUser = require("./controllers/users/register"); +const postUser = require("./controllers/users/registerAdmin"); const putUserDetails = require("./controllers/users/putUserDetails"); const authentication = require("./middlewares/authentication"); const authorise = require("./middlewares/authorisation"); -const getUserTypes = require("./controllers/users/userTypes"); -const { ADMIN } = require("~root/constants/userTypes"); + +const postRegistrationRequest = require("./controllers/users/postRegistrationRequest"); +const postCompleteRegistration = require("./controllers/users/postCompleteRegistration"); + +const getPasswordRecoveryRequestByShortcode = require("./controllers/users/getPasswordRecoveryRequestByShortcode"); const putPassword = require("./controllers/password-recovery/putPassword"); const postRecoveryRequest = require("./controllers/password-recovery/postRecoveryRequest"); +const postOrganizationInvitation = require("./controllers/invitations/postOrganizationInvitation"); +const getUserInvitations = require("./controllers/invitations/getUserInvitations"); +const acceptInvitation = require("./controllers/invitations/acceptInvitation"); +const patchUserInvitation = require("./controllers/users/patchUserInvitation"); +const getOrganizationUsers = require("./controllers/organizations/getOrganizationUsers"); +const getUserOrganizations = require("./controllers/users/getUserOrganizations"); + +const postOrganization = require("./controllers/organizations/postOrganization"); +const postOrganizationInvitations = require("./controllers/invitations/postOrganizationInvitations"); +const patchOrganizationInvitation = require("./controllers/invitations/patchOrganizationInvitation"); +const deleteInvitation = require("./controllers/invitations/deleteInvitation"); +const postOrganizationUser = require("./controllers/organizations/postOrganizationUser"); +const patchOrganizationUser = require("./controllers/organizations/patchOrganizationUser"); +const deleteOrganizationUser = require("./controllers/organizations/deleteOrganizationUser"); +const getOrganizationInvitations = require("./controllers/organizations/getOrganizationInvitations"); + +const postNewOrganization = require("./controllers/organizations/postNewOrganization"); +const getOrganizationsByEmail = require("./controllers/organizations/getOrganizationsByEmail"); +const getOrganizationsByUserId = require("./controllers/organizations/getOrganizationsByUserId"); +const getAllUsersByOrgId = require("./controllers/userOrganizations/getAllUsersByOrgId"); +const getUserOrganizationInvitations = require("./controllers/userOrganizationInvitations/getUserOrganizationInvitations"); +const deleteInvitationByOrgUser = require("./controllers/invitations/deleteInvitationByOrgUser"); +const postRegisterWithInvitation = require("./controllers/users/postRegisterWithInvitation"); +const getRegisterWithInvitation = require("./controllers/users/getRegisterWithInvitation"); + const router = express.Router(); -// USER MANAGEMENT router.post("/login", postLogin); - router.post( - "/register", + "/register/admin", authentication, authorise({ roles: [ADMIN] }), postUser ); - +router.post("/registration-request", postRegistrationRequest); +router.post( + "/complete-registration/:registrationShortcode", + postCompleteRegistration +); +router.post("/recovery-request", postRecoveryRequest); +router.get( + "/edit/user/request/shortcode/:shortcode", + getPasswordRecoveryRequestByShortcode +); +router.put("/update-password/:shortcode", putPassword); router.put("/edit/user", authentication, putUserDetails); -router.get("/user-types", getUserTypes); +router.post("/organizations/create", authentication, postOrganization); +router.get("/organizations/:orgId/users", authentication, getOrganizationUsers); +router.get("/users/organizations", authentication, getUserOrganizations); +router.post( + "/organizations/invitation", + authentication, + postOrganizationInvitation +); -router.post("/recovery-request", postRecoveryRequest); +router.patch("/user/invitation-details", authentication, patchUserInvitation); +router.post( + "/organizations/:orgId/invitations", + authentication, + postOrganizationInvitations +); +router.get( + "/organizations/:orgId/invitations", + authentication, + getOrganizationInvitations +); +router.patch( + "/organizations/:orgId/invitation", + authentication, + patchOrganizationInvitation +); +router.patch( + "/user/invitations/:invitationId", + authentication, + acceptInvitation +); +router.delete("/user/invitations", authentication, deleteInvitation); +router.delete( + "/organizations/invitations/:invitationId/:orgId", + authentication, + deleteInvitationByOrgUser +); +router.get("/user/invitations", authentication, getUserInvitations); +router.post("/organizations/users", authentication, postOrganizationUser); +router.patch( + "/organizations/:orgId/users/:userId", + authentication, + patchOrganizationUser +); +router.delete("/organizations/users", authentication, deleteOrganizationUser); +router.post("/organization", authentication, postNewOrganization); +router.get("/organizations/:email", getOrganizationsByEmail); +router.get("/organizations", authentication, getOrganizationsByUserId); +router.get("/organizations/:orgId/users", authentication, getAllUsersByOrgId); +router.get( + "/organizations/:orgId/invitations", + authentication, + getUserOrganizationInvitations +); -router.put("/update-password/:shortcode", putPassword); +router.post( + "/register-with-invitation/:invitationShortcode", + postRegisterWithInvitation +); + +router.get( + "/register-with-invitation/:invitationShortcode", + getRegisterWithInvitation +); module.exports = router; diff --git a/api-sample/src/lib/services/emails/sendEmail/templates/recovery-email/0.0.1.txt b/api-sample/src/lib/services/emails/sendEmail/templates/recovery-email/0.0.1.txt deleted file mode 100644 index d59203f8..00000000 --- a/api-sample/src/lib/services/emails/sendEmail/templates/recovery-email/0.0.1.txt +++ /dev/null @@ -1,11 +0,0 @@ -This e-mail is in response to your recent request to recover a forgotten password. This link will remain active for the next 8 hours to reset your password. - -Please click the link below and follow the instructions provided. -Recover Password -{{recoveryUrl}} - -In case you have any questions, feel free to get in touch at {{replyToEmail}}. - -Best Regards, -The Productive Machines Team --https://productivemachines.co.uk/ diff --git a/api-sample/src/lib/services/emails/sendEmail/index.js b/api-sample/src/services/emails/sendEmail/index.js similarity index 98% rename from api-sample/src/lib/services/emails/sendEmail/index.js rename to api-sample/src/services/emails/sendEmail/index.js index 1ff75be4..a3e709f0 100644 --- a/api-sample/src/lib/services/emails/sendEmail/index.js +++ b/api-sample/src/services/emails/sendEmail/index.js @@ -83,4 +83,4 @@ const sendMail = async ({ } }; -module.exports = sendMail; \ No newline at end of file +module.exports = sendMail; diff --git a/api-sample/src/services/emails/sendEmail/templates/company-manager-invitation/0.0.1.mjml b/api-sample/src/services/emails/sendEmail/templates/company-manager-invitation/0.0.1.mjml new file mode 100644 index 00000000..a5bdfb68 --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/company-manager-invitation/0.0.1.mjml @@ -0,0 +1,97 @@ + + + + + + + + + + + @import + url('https://fonts.googleapis.com/css2?family=Inter&family=Lato&family=Rubik:wght@400;500&display=swap'); + .no-link-decoration { color: #ffffff; text-decoration: none } + + + + + + + {{userRole}} Invitation + + + + + + {{sender}} invited you to become a + {{userRole}} + + + + this is only included is a message has been input when the invite was + sent. + + + + {{messageTitle}} + + + + + {{senderMessage}} + + + + + + + View Invitation + + + + diff --git a/api-sample/src/services/emails/sendEmail/templates/company-manager-invitation/0.0.1.subject b/api-sample/src/services/emails/sendEmail/templates/company-manager-invitation/0.0.1.subject new file mode 100644 index 00000000..6e10a0c5 --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/company-manager-invitation/0.0.1.subject @@ -0,0 +1 @@ +{{userRole}} Invitation \ No newline at end of file diff --git a/api-sample/src/services/emails/sendEmail/templates/company-manager-invitation/0.0.1.txt b/api-sample/src/services/emails/sendEmail/templates/company-manager-invitation/0.0.1.txt new file mode 100644 index 00000000..3ac0f5ab --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/company-manager-invitation/0.0.1.txt @@ -0,0 +1,7 @@ +{{userRole}} Invitation + +{{sender}} invited you to become a {{userRole}} for {{organizationName}} + +{{senderMessage}} + +View Invitation \ No newline at end of file diff --git a/api-sample/src/services/emails/sendEmail/templates/email-verification/0.0.1.mjml b/api-sample/src/services/emails/sendEmail/templates/email-verification/0.0.1.mjml new file mode 100644 index 00000000..da869eca --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/email-verification/0.0.1.mjml @@ -0,0 +1,79 @@ + + + + + + + + + + .no-link-decoration { color: #ffffff; text-decoration: none } + + + + + + + + + Complete Registration + + + + + + Hi {{name}}, please click link below to complete your registration. + + + + Complete Registration + + + + + + \ No newline at end of file diff --git a/api-sample/src/services/emails/sendEmail/templates/email-verification/0.0.1.subject b/api-sample/src/services/emails/sendEmail/templates/email-verification/0.0.1.subject new file mode 100644 index 00000000..b1325b1d --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/email-verification/0.0.1.subject @@ -0,0 +1 @@ +Complete Registration \ No newline at end of file diff --git a/api-sample/src/services/emails/sendEmail/templates/email-verification/0.0.1.txt b/api-sample/src/services/emails/sendEmail/templates/email-verification/0.0.1.txt new file mode 100644 index 00000000..ed0b1395 --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/email-verification/0.0.1.txt @@ -0,0 +1 @@ +Hi {{name}}, please click link below to complete your registration. \ No newline at end of file diff --git a/api-sample/src/services/emails/sendEmail/templates/invitation-status/0.0.1.mjml b/api-sample/src/services/emails/sendEmail/templates/invitation-status/0.0.1.mjml new file mode 100644 index 00000000..986ba579 --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/invitation-status/0.0.1.mjml @@ -0,0 +1,74 @@ + + + + + + + + + + + @import + url('https://fonts.googleapis.com/css2?family=Inter&family=Lato&family=Rubik:wght@400;500&display=swap'); + .no-link-decoration { color: #ffffff; text-decoration: none } + + + + + + + Company Manager Invitate Accepted + + + + + + {{sender}} {{invitationStatus}} your invite to become + {{userRole}} for + {{organizationName}} + + + + + + + View Invite + + + + diff --git a/api-sample/src/services/emails/sendEmail/templates/invitation-status/0.0.1.subject b/api-sample/src/services/emails/sendEmail/templates/invitation-status/0.0.1.subject new file mode 100644 index 00000000..7d1c0c3a --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/invitation-status/0.0.1.subject @@ -0,0 +1 @@ +{{userRole}} Invitation Accepted diff --git a/api-sample/src/services/emails/sendEmail/templates/invitation-status/0.0.1.txt b/api-sample/src/services/emails/sendEmail/templates/invitation-status/0.0.1.txt new file mode 100644 index 00000000..300bd281 --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/invitation-status/0.0.1.txt @@ -0,0 +1,7 @@ +{{userRole}} Invitation Accepted + +{{sender}} {{invitationStatus}} your invite to become + +{{userRole}} for {{organizationName}} + +View Invitation \ No newline at end of file diff --git a/api-sample/src/services/emails/sendEmail/templates/new-user-organization-invitation/0.0.1.mjml b/api-sample/src/services/emails/sendEmail/templates/new-user-organization-invitation/0.0.1.mjml new file mode 100644 index 00000000..3b4df895 --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/new-user-organization-invitation/0.0.1.mjml @@ -0,0 +1,95 @@ + + + + + + + + + + + @import + url('https://fonts.googleapis.com/css2?family=Inter&family=Lato&family=Rubik:wght@400;500&display=swap'); + .no-link-decoration { color: #ffffff; text-decoration: none } + + + + + + + Company Invite + + + + + + {{firstName}} {{lastName}} invited you to become a + {{userRole}} + of company. + + + + this is only included is a message has been input when the invite was + sent. + + + + Below is a message from {{firstName}} {{lastName}} + + + + + {{senderMessage}} + + + + + Please click here to register and + accept the invitation. + + + + + diff --git a/api-sample/src/services/emails/sendEmail/templates/new-user-organization-invitation/0.0.1.subject b/api-sample/src/services/emails/sendEmail/templates/new-user-organization-invitation/0.0.1.subject new file mode 100644 index 00000000..dfa7dc1c --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/new-user-organization-invitation/0.0.1.subject @@ -0,0 +1 @@ +Company Invite \ No newline at end of file diff --git a/api-sample/src/services/emails/sendEmail/templates/new-user-organization-invitation/0.0.1.txt b/api-sample/src/services/emails/sendEmail/templates/new-user-organization-invitation/0.0.1.txt new file mode 100644 index 00000000..e92e7261 --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/new-user-organization-invitation/0.0.1.txt @@ -0,0 +1,8 @@ +Company Invite + +{{sender}} invited you to become a {{userRole}} for {{organizationName}} + +Below is a message from {{sender}} +{{senderMessage}} + +Follow the link to register and accept invitation {{notificationsPageUrl}} \ No newline at end of file diff --git a/api-sample/src/services/emails/sendEmail/templates/password-confirmation/0.0.1.mjml b/api-sample/src/services/emails/sendEmail/templates/password-confirmation/0.0.1.mjml new file mode 100644 index 00000000..c29c17ec --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/password-confirmation/0.0.1.mjml @@ -0,0 +1,89 @@ + + + + + + + + + + .no-link-decoration { color: #ffffff; text-decoration: none } + + + + + + + + + Confirmation of Account Recovery + + + + + + Hi {{name}}, Your password has been successfully changed for the + following account; + + If you didn't change your password, follow this link to reset your + password to a new one. + + + + Reset Password + + + + + + diff --git a/api-sample/src/services/emails/sendEmail/templates/password-confirmation/0.0.1.subject b/api-sample/src/services/emails/sendEmail/templates/password-confirmation/0.0.1.subject new file mode 100644 index 00000000..290b5dc4 --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/password-confirmation/0.0.1.subject @@ -0,0 +1 @@ +Confirmation of Account Recovery \ No newline at end of file diff --git a/api-sample/src/services/emails/sendEmail/templates/password-confirmation/0.0.1.txt b/api-sample/src/services/emails/sendEmail/templates/password-confirmation/0.0.1.txt new file mode 100644 index 00000000..6b2f9692 --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/password-confirmation/0.0.1.txt @@ -0,0 +1,12 @@ +Confirmation of Account Recovery + + Hi {{name}}, Your password has been successfully changed for the following account; +Just click the link below to get started: + +If you didn't change your password, follow this link to reset your password to a new one. +In case you have any questions, feel free to get in touch at {{replyToEmail}}. + +Recover Password + +Best regards, +Guestlist Team diff --git a/api-sample/src/lib/services/emails/sendEmail/templates/recovery-email/0.0.1.mjml b/api-sample/src/services/emails/sendEmail/templates/recovery-email/0.0.1.mjml similarity index 99% rename from api-sample/src/lib/services/emails/sendEmail/templates/recovery-email/0.0.1.mjml rename to api-sample/src/services/emails/sendEmail/templates/recovery-email/0.0.1.mjml index a1e80a04..6758d1f9 100644 --- a/api-sample/src/lib/services/emails/sendEmail/templates/recovery-email/0.0.1.mjml +++ b/api-sample/src/services/emails/sendEmail/templates/recovery-email/0.0.1.mjml @@ -25,7 +25,6 @@ padding-bottom="15px" padding-top="20px" > - + + + + + + + + + + @import + url('https://fonts.googleapis.com/css2?family=Inter&family=Lato&family=Rubik:wght@400;500&display=swap'); + .no-link-decoration { color: #ffffff; text-decoration: none } + + + + + + + Company Invite + + + + + + {{firstName}} {{lastName}} invited you to become a + {{userRole}} + of company. + + + + this is only included is a message has been input when the invite was + sent. + + + + Below is a message from {{firstName}} {{lastName}} + + + + + {{senderMessage}} + + + + + Please click here to login and + view the invitation. + + + + + diff --git a/api-sample/src/services/emails/sendEmail/templates/user-organization-invitation/0.0.1.subject b/api-sample/src/services/emails/sendEmail/templates/user-organization-invitation/0.0.1.subject new file mode 100644 index 00000000..dfa7dc1c --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/user-organization-invitation/0.0.1.subject @@ -0,0 +1 @@ +Company Invite \ No newline at end of file diff --git a/api-sample/src/services/emails/sendEmail/templates/user-organization-invitation/0.0.1.txt b/api-sample/src/services/emails/sendEmail/templates/user-organization-invitation/0.0.1.txt new file mode 100644 index 00000000..75dd3104 --- /dev/null +++ b/api-sample/src/services/emails/sendEmail/templates/user-organization-invitation/0.0.1.txt @@ -0,0 +1,8 @@ +Company Invite + +{{sender}} invited you to become a {{userRole}} for {{organizationName}} + +Below is a message from {{sender}} +{{senderMessage}} + +Follow the link to login and view invitation {{notificationsPageUrl}} \ No newline at end of file diff --git a/cli/utils/writeDotEnvFile.js b/cli/utils/writeDotEnvFile.js index 01821633..3f97a6bf 100644 --- a/cli/utils/writeDotEnvFile.js +++ b/cli/utils/writeDotEnvFile.js @@ -18,10 +18,9 @@ PASSWORD_SALT=SECRET_SALT // MINIO_SECRET_KEY= // mailgun credentials -// MAILGUN_API_KEY= -// MAILGUN_API_BASE_URL= -// MAILGUN_DOMAIN= - +MAILGUN_API_KEY=change_me +MAILGUN_API_BASE_URL=change_me +MAILGUN_DOMAIN=change_me `; const filePath = path.join(projectRoot, "./.env"); const doesItExist = fs.ensureFileSync(filePath); diff --git a/package.json b/package.json index b25ef281..f2585531 100644 --- a/package.json +++ b/package.json @@ -48,4 +48,4 @@ "url": "https://github.com/CyprusCodes/xest/issues" }, "homepage": "https://github.com/CyprusCodes/xest#readme" -} +} \ No newline at end of file