From ebfab252fcac2a9db5e3b1a7c40f1bdc1fc80bdc Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Fri, 8 Mar 2019 19:51:08 +0100 Subject: [PATCH] Add system/applications - CRUD support for applications - new (application related) permissions --- api/system/spec.json | 89 ++++++ api/system/spec/application.json | 132 ++++++++ codegen.sh | 1 + docs/system/README.md | 81 +++++ system/db/mysql/statik.go | 2 +- .../mysql/20190306205033.applications.up.sql | 40 +++ system/repository/application.go | 85 +++++ system/repository/applications_test.go | 66 ++++ system/rest/application.go | 71 +++++ system/rest/handlers/application.go | 97 ++++++ system/rest/request/application.go | 290 ++++++++++++++++++ system/rest/router.go | 1 + system/service/application.go | 90 ++++++ system/service/service.go | 2 + system/service/validation.go | 6 + system/types/application.gen.go | 67 ++++ system/types/applications.go | 59 ++++ 17 files changed, 1178 insertions(+), 1 deletion(-) create mode 100644 api/system/spec/application.json create mode 100644 system/db/schema/mysql/20190306205033.applications.up.sql create mode 100644 system/repository/application.go create mode 100644 system/repository/applications_test.go create mode 100644 system/rest/application.go create mode 100644 system/rest/handlers/application.go create mode 100644 system/rest/request/application.go create mode 100644 system/service/application.go create mode 100644 system/types/application.gen.go create mode 100644 system/types/applications.go diff --git a/api/system/spec.json b/api/system/spec.json index 2abd5b7bd..0505e46eb 100644 --- a/api/system/spec.json +++ b/api/system/spec.json @@ -574,6 +574,95 @@ } ] }, + { + "title": "Applications", + "path": "/application", + "entrypoint": "application", + "authentication": [], + "struct": [ + { + "imports": [ + "sqlxTypes github.com/jmoiron/sqlx/types" + ] + } + ], + "apis": [ + { + "name": "list", + "method": "GET", + "title": "List applications", + "path": "/" + }, + { + "name": "create", + "method": "POST", + "title": "Create application", + "path": "/", + "parameters": { + "post": [ + { "name": "name", "type": "string", "required": true, "title": "Email"}, + { "name": "enabled", "type": "bool", "required": false, "title": "Enabled"}, + { "name": "unify", "type": "sqlxTypes.JSONText", "required": false, "title": "Unify properties"}, + { "name": "config", "type": "sqlxTypes.JSONText", "required": false, "title": "Arbitrary JSON holding application configuration"} + ] + } + }, + { + "name": "update", + "method": "PUT", + "title": "Update user details", + "path": "/{applicationID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "applicationID", + "required": true, + "title": "Application ID" + } + ], + "post": [ + { "name": "name", "type": "string", "required": true, "title": "Email"}, + { "name": "enabled", "type": "bool", "required": false, "title": "Enabled"}, + { "name": "unify", "type": "sqlxTypes.JSONText", "required": false, "title": "Unify properties"}, + { "name": "config", "type": "sqlxTypes.JSONText", "required": false, "title": "Arbitrary JSON holding application configuration"} + ] + } + }, + { + "name": "read", + "method": "GET", + "title": "Read application details", + "path": "/{applicationID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "applicationID", + "required": true, + "title": "Application ID" + } + ] + } + }, + { + "name": "delete", + "method": "DELETE", + "title": "Remove application", + "path": "/{applicationID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "applicationID", + "required": true, + "title": "Application ID" + } + ] + } + } + ] + }, { "title": "Permissions", "parameters": {}, diff --git a/api/system/spec/application.json b/api/system/spec/application.json new file mode 100644 index 000000000..f7b1453ab --- /dev/null +++ b/api/system/spec/application.json @@ -0,0 +1,132 @@ +{ + "Title": "Applications", + "Interface": "Application", + "Struct": [ + { + "imports": [ + "sqlxTypes github.com/jmoiron/sqlx/types" + ] + } + ], + "Parameters": null, + "Protocol": "", + "Authentication": [], + "Path": "/application", + "APIs": [ + { + "Name": "list", + "Method": "GET", + "Title": "List applications", + "Path": "/", + "Parameters": null + }, + { + "Name": "create", + "Method": "POST", + "Title": "Create application", + "Path": "/", + "Parameters": { + "post": [ + { + "name": "name", + "required": true, + "title": "Email", + "type": "string" + }, + { + "name": "enabled", + "required": false, + "title": "Enabled", + "type": "bool" + }, + { + "name": "unify", + "required": false, + "title": "Unify properties", + "type": "sqlxTypes.JSONText" + }, + { + "name": "config", + "required": false, + "title": "Arbitrary JSON holding application configuration", + "type": "sqlxTypes.JSONText" + } + ] + } + }, + { + "Name": "update", + "Method": "PUT", + "Title": "Update user details", + "Path": "/{applicationID}", + "Parameters": { + "path": [ + { + "name": "applicationID", + "required": true, + "title": "Application ID", + "type": "uint64" + } + ], + "post": [ + { + "name": "name", + "required": true, + "title": "Email", + "type": "string" + }, + { + "name": "enabled", + "required": false, + "title": "Enabled", + "type": "bool" + }, + { + "name": "unify", + "required": false, + "title": "Unify properties", + "type": "sqlxTypes.JSONText" + }, + { + "name": "config", + "required": false, + "title": "Arbitrary JSON holding application configuration", + "type": "sqlxTypes.JSONText" + } + ] + } + }, + { + "Name": "read", + "Method": "GET", + "Title": "Read application details", + "Path": "/{applicationID}", + "Parameters": { + "path": [ + { + "name": "applicationID", + "required": true, + "title": "Application ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "delete", + "Method": "DELETE", + "Title": "Remove application", + "Path": "/{applicationID}", + "Parameters": { + "path": [ + { + "name": "applicationID", + "required": true, + "title": "Application ID", + "type": "uint64" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/codegen.sh b/codegen.sh index 439e82ef6..0b1709a58 100755 --- a/codegen.sh +++ b/codegen.sh @@ -39,6 +39,7 @@ function types { ./build/gen-type-set --with-primary-key=false --types Unread --output messaging/types/unread.gen.go ./build/gen-type-set --types User --output system/types/user.gen.go + ./build/gen-type-set --types Application --output system/types/application.gen.go ./build/gen-type-set --with-resources=true --resource-type "rules.Resource" --imports "github.com/crusttech/crust/internal/rules" --types Role --output system/types/role.gen.go ./build/gen-type-set --with-resources=true --resource-type "rules.Resource" --imports "github.com/crusttech/crust/internal/rules" --types Organisation --output system/types/organisation.gen.go ./build/gen-type-set --types Credentials --output system/types/credentials.gen.go diff --git a/docs/system/README.md b/docs/system/README.md index 25cba58e2..15a0492df 100644 --- a/docs/system/README.md +++ b/docs/system/README.md @@ -1,3 +1,84 @@ +# Applications + +## List applications + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/application/` | HTTP/S | GET | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | + +## Create application + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/application/` | HTTP/S | POST | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| name | string | POST | Email | N/A | YES | +| enabled | bool | POST | Enabled | N/A | NO | +| unify | sqlxTypes.JSONText | POST | Unify properties | N/A | NO | +| config | sqlxTypes.JSONText | POST | Arbitrary JSON holding application configuration | N/A | NO | + +## Update user details + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/application/{applicationID}` | HTTP/S | PUT | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| applicationID | uint64 | PATH | Application ID | N/A | YES | +| name | string | POST | Email | N/A | YES | +| enabled | bool | POST | Enabled | N/A | NO | +| unify | sqlxTypes.JSONText | POST | Unify properties | N/A | NO | +| config | sqlxTypes.JSONText | POST | Arbitrary JSON holding application configuration | N/A | NO | + +## Read application details + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/application/{applicationID}` | HTTP/S | GET | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| applicationID | uint64 | PATH | Application ID | N/A | YES | + +## Remove application + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/application/{applicationID}` | HTTP/S | DELETE | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| applicationID | uint64 | PATH | Application ID | N/A | YES | + + + + # Authentication ## Check JWT token diff --git a/system/db/mysql/statik.go b/system/db/mysql/statik.go index 24e2793c4..b683faaa2 100644 --- a/system/db/mysql/statik.go +++ b/system/db/mysql/statik.go @@ -8,7 +8,7 @@ import ( ) func Data() string { - return "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8-- all known organisations (crust instances) and our relation towards them\nCREATE TABLE organisations (\n id BIGINT UNSIGNED NOT NULL,\n fqn TEXT NOT NULL, -- fully qualified name of the organisation\n name TEXT NOT NULL, -- display name of the organisation\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- organisation soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE settings (\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value TEXT COMMENT 'Setting value',\n\n PRIMARY KEY (name)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE users (\n id BIGINT UNSIGNED NOT NULL,\n email TEXT NOT NULL,\n username TEXT NOT NULL,\n password TEXT NOT NULL,\n name TEXT NOT NULL,\n handle TEXT NOT NULL,\n meta JSON NOT NULL,\n satosa_id CHAR(36) NULL,\n\n rel_organisation BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n suspended_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE UNIQUE INDEX uid_satosa ON users (satosa_id);\n\n-- Keeps all known teams\nCREATE TABLE teams (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the team\n handle TEXT NOT NULL, -- team handle string\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- team soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps team memberships\nCREATE TABLE team_members (\n rel_team BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (rel_team, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xedzU\x8am \x00\x00m \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE teams RENAME TO sys_team;\nALTER TABLE organisations RENAME TO sys_organisation;\nALTER TABLE team_members RENAME TO sys_team_member;\nALTER TABLE users RENAME TO sys_user;PK\x07\x08\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8# add field to manage user type (bot support)\nALTER TABLE `sys_user` ADD `kind` VARCHAR(8) NOT NULL DEFAULT '' AFTER `handle`;\n\n# add field to manage \"ownership\" (get all bots created by user)\nALTER TABLE `sys_user` ADD `rel_user_id` BIGINT UNSIGNED NOT NULL AFTER `rel_organisation`, ADD INDEX (`rel_user_id`);\nPK\x07\x089\xa0\xdat8\x01\x00\x008\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP INDEX `uid_satosa`, ADD INDEX `uid_satosa` (`satosa_id`) USING BTREE;PK\x07\x08\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE sys_credentials (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n label TEXT NOT NULL COMMENT 'something we can differentiate credentials by',\n kind VARCHAR(128) NOT NULL COMMENT 'hash, facebook, gplus, github, linkedin ...',\n credentials TEXT NOT NULL COMMENT 'crypted/hashed passwords, secrets, social profile ID',\n meta JSON NOT NULL,\n expires_at DATETIME NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX idx_owner ON sys_credentials (rel_owner);\nPK\x07\x08f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` MODIFY `password` TEXT NULL;\nPK\x07\x080V\x13\x0f4\x00\x00\x004\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `sys_rules` (\n `rel_team` BIGINT UNSIGNED NOT NULL,\n `resource` VARCHAR(128) NOT NULL,\n `operation` VARCHAR(128) NOT NULL,\n `value` TINYINT(1) NOT NULL,\n\n PRIMARY KEY (`rel_team`, `resource`, `operation`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE sys_team RENAME TO sys_role;\nALTER TABLE sys_team_member RENAME TO sys_role_member;\n\nALTER TABLE `sys_role_member` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `sys_rules` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nPK\x07\x08s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00,\x00 \x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `sys_role` (`id`, `name`, `handle`) VALUES (1, 'Everyone', 'everyone');\nINSERT INTO `sys_role` (`id`, `name`, `handle`) VALUES (2, 'Administrators', 'admins');\n\n-- Value: Allow (2), Deny (1), Inherit(0),\nINSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n-- Everyone\n (1, 'compose', 'access', 2),\n (1, 'messaging', 'access', 2),\n-- Admins\n (2, 'compose', 'namespace.create', 2),\n (2, 'compose', 'access', 2),\n (2, 'compose', 'grant', 2),\n (2, 'compose:namespace', 'page.create', 2),\n (2, 'compose:namespace', 'read', 2),\n (2, 'compose:namespace', 'update', 2),\n (2, 'compose:namespace', 'delete', 2),\n (2, 'compose:namespace', 'module.create', 2),\n (2, 'compose:namespace', 'chart.create', 2),\n (2, 'compose:namespace', 'trigger.create', 2),\n (2, 'compose:chart', 'read', 2),\n (2, 'compose:chart', 'update', 2),\n (2, 'compose:chart', 'delete', 2),\n (2, 'compose:trigger', 'read', 2),\n (2, 'compose:trigger', 'update', 2),\n (2, 'compose:trigger', 'delete', 2),\n (2, 'compose:page', 'read', 2),\n (2, 'compose:page', 'update', 2),\n (2, 'compose:page', 'delete', 2),\n (2, 'system', 'grant', 2),\n (2, 'system', 'organisation.create', 2),\n (2, 'system', 'role.create', 2),\n (2, 'system:organisation', 'access', 2),\n (2, 'system:role', 'read', 2),\n (2, 'system:role', 'update', 2),\n (2, 'system:role', 'delete', 2),\n (2, 'system:role', 'members.manage', 2),\n (2, 'messaging', 'access', 2),\n (2, 'messaging', 'grant', 2),\n (2, 'messaging', 'channel.public.create', 2),\n (2, 'messaging', 'channel.private.create', 2),\n (2, 'messaging', 'channel.direct.create', 2),\n (2, 'messaging:channel', 'update', 2),\n (2, 'messaging:channel', 'message.attach', 2),\n (2, 'messaging:channel', 'message.update.all', 2),\n (2, 'messaging:channel', 'leave', 2),\n (2, 'messaging:channel', 'webhooks.manage', 2),\n (2, 'messaging:channel', 'message.embed', 2),\n (2, 'messaging:channel', 'members.manage', 2),\n (2, 'messaging:channel', 'attachments.manage', 2),\n (2, 'messaging:channel', 'message.send', 2),\n (2, 'messaging:channel', 'message.reply', 2),\n (2, 'messaging:channel', 'read', 2),\n (2, 'messaging:channel', 'join', 2),\n (2, 'messaging:channel', 'message.update.own', 2),\n (2, 'messaging:channel', 'message.react', 2),\n (2, 'compose:module', 'read', 2),\n (2, 'compose:module', 'update', 2),\n (2, 'compose:module', 'delete', 2),\n (2, 'compose:module', 'record.create', 2),\n (2, 'compose:module', 'record.read', 2),\n (2, 'compose:module', 'record.update', 2),\n (2, 'compose:module', 'record.delete', 2);\nPK\x07\x08\x186!\x1c\x10\n\x00\x00\x10\n\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xedzU\x8am \x00\x00m \x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe \x00\x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9\xa0\xdat8\x01\x00\x008\x01\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd8\n\x00\x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81t\x0c\x00\x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x0d\x00\x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(0V\x13\x0f4\x00\x00\x004\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81+\x11\x00\x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbf\x11\x00\x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x16\x13\x00\x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x186!\x1c\x10\n\x00\x00\x10\n\x00\x00,\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x89\x14\x00\x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xfc\x1e\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81\xb9 \x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x0b\x00\x0b\x00\xcd\x03\x00\x00$!\x00\x00\x00\x00" + return "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8-- all known organisations (crust instances) and our relation towards them\nCREATE TABLE organisations (\n id BIGINT UNSIGNED NOT NULL,\n fqn TEXT NOT NULL, -- fully qualified name of the organisation\n name TEXT NOT NULL, -- display name of the organisation\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- organisation soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE settings (\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value TEXT COMMENT 'Setting value',\n\n PRIMARY KEY (name)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE users (\n id BIGINT UNSIGNED NOT NULL,\n email TEXT NOT NULL,\n username TEXT NOT NULL,\n password TEXT NOT NULL,\n name TEXT NOT NULL,\n handle TEXT NOT NULL,\n meta JSON NOT NULL,\n satosa_id CHAR(36) NULL,\n\n rel_organisation BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n suspended_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE UNIQUE INDEX uid_satosa ON users (satosa_id);\n\n-- Keeps all known teams\nCREATE TABLE teams (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the team\n handle TEXT NOT NULL, -- team handle string\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- team soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps team memberships\nCREATE TABLE team_members (\n rel_team BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (rel_team, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xedzU\x8am \x00\x00m \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE teams RENAME TO sys_team;\nALTER TABLE organisations RENAME TO sys_organisation;\nALTER TABLE team_members RENAME TO sys_team_member;\nALTER TABLE users RENAME TO sys_user;PK\x07\x08\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8# add field to manage user type (bot support)\nALTER TABLE `sys_user` ADD `kind` VARCHAR(8) NOT NULL DEFAULT '' AFTER `handle`;\n\n# add field to manage \"ownership\" (get all bots created by user)\nALTER TABLE `sys_user` ADD `rel_user_id` BIGINT UNSIGNED NOT NULL AFTER `rel_organisation`, ADD INDEX (`rel_user_id`);\nPK\x07\x089\xa0\xdat8\x01\x00\x008\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP INDEX `uid_satosa`, ADD INDEX `uid_satosa` (`satosa_id`) USING BTREE;PK\x07\x08\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE sys_credentials (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n label TEXT NOT NULL COMMENT 'something we can differentiate credentials by',\n kind VARCHAR(128) NOT NULL COMMENT 'hash, facebook, gplus, github, linkedin ...',\n credentials TEXT NOT NULL COMMENT 'crypted/hashed passwords, secrets, social profile ID',\n meta JSON NOT NULL,\n expires_at DATETIME NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX idx_owner ON sys_credentials (rel_owner);\nPK\x07\x08f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` MODIFY `password` TEXT NULL;\nPK\x07\x080V\x13\x0f4\x00\x00\x004\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `sys_rules` (\n `rel_team` BIGINT UNSIGNED NOT NULL,\n `resource` VARCHAR(128) NOT NULL,\n `operation` VARCHAR(128) NOT NULL,\n `value` TINYINT(1) NOT NULL,\n\n PRIMARY KEY (`rel_team`, `resource`, `operation`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE sys_team RENAME TO sys_role;\nALTER TABLE sys_team_member RENAME TO sys_role_member;\n\nALTER TABLE `sys_role_member` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `sys_rules` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nPK\x07\x08s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00,\x00 \x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `sys_role` (`id`, `name`, `handle`) VALUES (1, 'Everyone', 'everyone');\nINSERT INTO `sys_role` (`id`, `name`, `handle`) VALUES (2, 'Administrators', 'admins');\n\n-- Value: Allow (2), Deny (1), Inherit(0),\nINSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n-- Everyone\n (1, 'compose', 'access', 2),\n (1, 'messaging', 'access', 2),\n-- Admins\n (2, 'compose', 'namespace.create', 2),\n (2, 'compose', 'access', 2),\n (2, 'compose', 'grant', 2),\n (2, 'compose:namespace', 'page.create', 2),\n (2, 'compose:namespace', 'read', 2),\n (2, 'compose:namespace', 'update', 2),\n (2, 'compose:namespace', 'delete', 2),\n (2, 'compose:namespace', 'module.create', 2),\n (2, 'compose:namespace', 'chart.create', 2),\n (2, 'compose:namespace', 'trigger.create', 2),\n (2, 'compose:chart', 'read', 2),\n (2, 'compose:chart', 'update', 2),\n (2, 'compose:chart', 'delete', 2),\n (2, 'compose:trigger', 'read', 2),\n (2, 'compose:trigger', 'update', 2),\n (2, 'compose:trigger', 'delete', 2),\n (2, 'compose:page', 'read', 2),\n (2, 'compose:page', 'update', 2),\n (2, 'compose:page', 'delete', 2),\n (2, 'system', 'grant', 2),\n (2, 'system', 'organisation.create', 2),\n (2, 'system', 'role.create', 2),\n (2, 'system:organisation', 'access', 2),\n (2, 'system:role', 'read', 2),\n (2, 'system:role', 'update', 2),\n (2, 'system:role', 'delete', 2),\n (2, 'system:role', 'members.manage', 2),\n (2, 'messaging', 'access', 2),\n (2, 'messaging', 'grant', 2),\n (2, 'messaging', 'channel.public.create', 2),\n (2, 'messaging', 'channel.private.create', 2),\n (2, 'messaging', 'channel.direct.create', 2),\n (2, 'messaging:channel', 'update', 2),\n (2, 'messaging:channel', 'message.attach', 2),\n (2, 'messaging:channel', 'message.update.all', 2),\n (2, 'messaging:channel', 'leave', 2),\n (2, 'messaging:channel', 'webhooks.manage', 2),\n (2, 'messaging:channel', 'message.embed', 2),\n (2, 'messaging:channel', 'members.manage', 2),\n (2, 'messaging:channel', 'attachments.manage', 2),\n (2, 'messaging:channel', 'message.send', 2),\n (2, 'messaging:channel', 'message.reply', 2),\n (2, 'messaging:channel', 'read', 2),\n (2, 'messaging:channel', 'join', 2),\n (2, 'messaging:channel', 'message.update.own', 2),\n (2, 'messaging:channel', 'message.react', 2),\n (2, 'compose:module', 'read', 2),\n (2, 'compose:module', 'update', 2),\n (2, 'compose:module', 'delete', 2),\n (2, 'compose:module', 'record.create', 2),\n (2, 'compose:module', 'record.read', 2),\n (2, 'compose:module', 'record.update', 2),\n (2, 'compose:module', 'record.delete', 2);\nPK\x07\x08\x186!\x1c\x10\n\x00\x00\x10\n\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00 \x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE sys_application (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n name TEXT NOT NULL COMMENT 'something we can differentiate application by',\n enabled BOOL NOT NULL,\n\n unify JSON NULL COMMENT 'unify specific settings',\n config JSON NULL COMMENT 'application configuration',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\nREPLACE INTO `sys_application` (`id`, `name`, `enabled`, `rel_owner`, `unify`) VALUES\n( 1, 'Crust Messaging', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/messaging/\"}'\n),\n( 2, 'Crust CRM', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/crm/\"}'\n),\n( 3, 'Crust Admin Area', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/admin/\"}'\n),\n( 4, 'Corteza Jitsi Bridge', true, 0,\n '{\"logo\": \"/applications/jitsi.jpg\", \"icon\": \"/applications/jitsi_icon.png\", \"url\": \"/bridge/jitsi/\"}'\n),\n( 5, 'Google Maps', true, 0,\n '{\"logo\": \"/applications/google_maps.jpg\", \"icon\": \"/applications/google_maps_icon.png\", \"url\": \"https://maps.google.com/maps?width=100%&height=600&hl=es&q=Europe&ie=UTF8&t=&z=4&iwloc=B&output=embed\"}'\n);\nPK\x07\x08Q\x9c\x18\x1f\x17\x06\x00\x00\x17\x06\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xedzU\x8am \x00\x00m \x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe \x00\x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9\xa0\xdat8\x01\x00\x008\x01\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd8\n\x00\x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81t\x0c\x00\x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x0d\x00\x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(0V\x13\x0f4\x00\x00\x004\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81+\x11\x00\x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbf\x11\x00\x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x16\x13\x00\x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x186!\x1c\x10\n\x00\x00\x10\n\x00\x00,\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x89\x14\x00\x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(Q\x9c\x18\x1f\x17\x06\x00\x00\x17\x06\x00\x00\"\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xfc\x1e\x00\x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81l%\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81)'\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x0c\x00\x0c\x00&\x04\x00\x00\x94'\x00\x00\x00\x00" } func init() { diff --git a/system/db/schema/mysql/20190306205033.applications.up.sql b/system/db/schema/mysql/20190306205033.applications.up.sql new file mode 100644 index 000000000..c626f7499 --- /dev/null +++ b/system/db/schema/mysql/20190306205033.applications.up.sql @@ -0,0 +1,40 @@ +CREATE TABLE sys_application ( + id BIGINT UNSIGNED NOT NULL, + rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id), + name TEXT NOT NULL COMMENT 'something we can differentiate application by', + enabled BOOL NOT NULL, + + unify JSON NULL COMMENT 'unify specific settings', + + created_at DATETIME NOT NULL DEFAULT NOW(), + updated_at DATETIME NULL, + deleted_at DATETIME NULL, -- user soft delete + + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +REPLACE INTO `sys_application` (`id`, `name`, `enabled`, `rel_owner`, `unify`) VALUES +( 1, 'Crust Messaging', true, 0, + '{"logo": "/applications/crust.jpg", "icon": "/applications/crust_favicon.png", "url": "/messaging/", "listed": true}' +), +( 2, 'Crust CRM', true, 0, + '{"logo": "/applications/crust.jpg", "icon": "/applications/crust_favicon.png", "url": "/crm/", "listed": true}' +), +( 3, 'Crust Admin Area', true, 0, + '{"logo": "/applications/crust.jpg", "icon": "/applications/crust_favicon.png", "url": "/admin/", "listed": true}' +), +( 4, 'Corteza Jitsi Bridge', true, 0, + '{"logo": "/applications/jitsi.png", "icon": "/applications/jitsi_icon.png", "url": "/bridge/jitsi/", "listed": true}' +), +( 5, 'Google Maps', true, 0, + '{"logo": "/applications/google_maps.png", "icon": "/applications/google_maps_icon.png", "url": "/bridge/google-maps/", "listed": true}' +); + +-- Allow admin access to applications +INSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES + (2, 'system', 'application.create', 2), + (2, 'application:role', 'read', 2), + (2, 'application:role', 'update', 2), + (2, 'application:role', 'delete', 2) +; diff --git a/system/repository/application.go b/system/repository/application.go new file mode 100644 index 000000000..7e6154538 --- /dev/null +++ b/system/repository/application.go @@ -0,0 +1,85 @@ +package repository + +import ( + "context" + "time" + + "github.com/titpetric/factory" + + "github.com/crusttech/crust/system/types" +) + +type ( + ApplicationRepository interface { + With(ctx context.Context, db *factory.DB) ApplicationRepository + + FindByID(id uint64) (*types.Application, error) + Find() (types.ApplicationSet, error) + + Create(mod *types.Application) (*types.Application, error) + Update(mod *types.Application) (*types.Application, error) + + DeleteByID(id uint64) error + } + + application struct { + *repository + + // sql table reference + applications string + members string + } +) + +const ( + sqlApplicationColumns = "id, rel_owner, name, enabled, unify, created_at, updated_at, deleted_at" + sqlApplicationScope = "deleted_at IS NULL" + + ErrApplicationNotFound = repositoryError("ApplicationNotFound") +) + +func Application(ctx context.Context, db *factory.DB) ApplicationRepository { + return (&application{}).With(ctx, db) +} + +func (r *application) With(ctx context.Context, db *factory.DB) ApplicationRepository { + return &application{ + repository: r.repository.With(ctx, db), + applications: "sys_application", + } +} + +func (r *application) FindByID(id uint64) (*types.Application, error) { + sql := "SELECT " + sqlApplicationColumns + " FROM " + r.applications + " WHERE id = ? AND " + sqlApplicationScope + mod := &types.Application{} + + return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrApplicationNotFound) +} + +func (r *application) Find() (types.ApplicationSet, error) { + rval := make([]*types.Application, 0) + params := make([]interface{}, 0) + + sql := "SELECT " + sqlApplicationColumns + " FROM " + r.applications + " WHERE " + sqlApplicationScope + + sql += " ORDER BY id ASC" + + return rval, r.db().Select(&rval, sql, params...) +} + +func (r *application) Create(mod *types.Application) (*types.Application, error) { + mod.ID = factory.Sonyflake.NextID() + mod.CreatedAt = time.Now() + + return mod, r.db().Insert(r.applications, mod) +} + +func (r *application) Update(mod *types.Application) (*types.Application, error) { + mod.UpdatedAt = timeNowPtr() + + return mod, r.db().Replace(r.applications, mod) +} + +func (r *application) DeleteByID(id uint64) error { + return r.updateColumnByID(r.applications, "deleted_at", time.Now(), id) +} diff --git a/system/repository/applications_test.go b/system/repository/applications_test.go new file mode 100644 index 000000000..29dffd272 --- /dev/null +++ b/system/repository/applications_test.go @@ -0,0 +1,66 @@ +package repository + +import ( + "context" + "testing" + + "github.com/titpetric/factory" + + "github.com/crusttech/crust/internal/test" + "github.com/crusttech/crust/system/types" +) + +func TestApplication(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + return + } + + crepo := Application(context.Background(), factory.Database.MustGet()) + + { + + tx(t, func() (err error) { + if _, err = factory.Database.MustGet().Exec("TRUNCATE sys_application"); err != nil { + return + } + + app := &types.Application{ + Name: "created", + Enabled: true, + OwnerID: 1, + Unify: &types.ApplicationUnify{ + Name: "created", + Listed: true, + Order: 1, + Icon: "...ico", + }, + } + + app, err = crepo.Create(app) + test.NoError(t, err, "Application.Create error: %+v", err) + test.Assert(t, app.Valid(), "Expecting application to be valid after creation") + test.Assert(t, app.Name == "created", "Expecting application name to be set, got %q", app.Name) + test.Assert(t, app.Enabled, "Expecting application to be enabled") + test.Assert(t, app.Unify.Name == "created", "Expecting application name to be set in unify, got %q", app.Name) + test.Assert(t, app.Unify.Listed, "Expecting application to be listed in unify") + test.Assert(t, app.Unify.Order == 1, "Expecting application name to have order val 1") + + app.Name = "updated" + app.Enabled = false + app.Unify.Name = "updated" + app.Unify.Listed = false + app, err = crepo.Update(app) + + test.NoError(t, err, "Application.Create error: %+v", err) + test.Assert(t, err == nil, "Application.Create error: %+v", err) + test.Assert(t, app.Name == "updated", "Expecting application name to be updated") + test.Assert(t, !app.Enabled, "Expecting application to be disabled") + test.Assert(t, app.Unify.Name == "updated", "Expecting application name to be updated in unify") + test.Assert(t, !app.Unify.Listed, "Expecting application to be unlisted in unify") + + return nil + }) + } + +} diff --git a/system/rest/application.go b/system/rest/application.go new file mode 100644 index 000000000..136113562 --- /dev/null +++ b/system/rest/application.go @@ -0,0 +1,71 @@ +package rest + +import ( + "context" + + "github.com/crusttech/crust/system/rest/request" + "github.com/crusttech/crust/system/service" + "github.com/crusttech/crust/system/types" + + "github.com/pkg/errors" +) + +var _ = errors.Wrap + +type Application struct { + svc struct { + application service.ApplicationService + } +} + +func (Application) New() *Application { + ctrl := &Application{} + ctrl.svc.application = service.DefaultApplication + + return ctrl +} + +func (ctrl *Application) List(ctx context.Context, r *request.ApplicationList) (interface{}, error) { + return ctrl.svc.application.With(ctx).Find() +} + +func (ctrl *Application) Create(ctx context.Context, r *request.ApplicationCreate) (interface{}, error) { + app := &types.Application{ + Name: r.Name, + Enabled: r.Enabled, + } + + if r.Unify != nil { + app.Unify = &types.ApplicationUnify{} + if err := r.Unify.Unmarshal(app.Unify); err != nil { + return nil, err + } + } + + return ctrl.svc.application.With(ctx).Create(app) +} + +func (ctrl *Application) Update(ctx context.Context, r *request.ApplicationUpdate) (interface{}, error) { + app := &types.Application{ + ID: r.ApplicationID, + Name: r.Name, + Enabled: r.Enabled, + } + + if r.Unify != nil { + app.Unify = &types.ApplicationUnify{} + if err := r.Unify.Unmarshal(app.Unify); err != nil { + return nil, err + } + } + + return ctrl.svc.application.With(ctx).Update(app) +} + +func (ctrl *Application) Read(ctx context.Context, r *request.ApplicationRead) (interface{}, error) { + return ctrl.svc.application.With(ctx).FindByID(r.ApplicationID) +} + +func (ctrl *Application) Delete(ctx context.Context, r *request.ApplicationDelete) (interface{}, error) { + return nil, ctrl.svc.application.With(ctx).DeleteByID(r.ApplicationID) +} diff --git a/system/rest/handlers/application.go b/system/rest/handlers/application.go new file mode 100644 index 000000000..8b7b0d394 --- /dev/null +++ b/system/rest/handlers/application.go @@ -0,0 +1,97 @@ +package handlers + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `application.go`, `application.util.go` or `application_test.go` to + implement your API calls, helper functions and tests. The file `application.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "context" + "github.com/go-chi/chi" + "net/http" + + "github.com/titpetric/factory/resputil" + + "github.com/crusttech/crust/system/rest/request" +) + +// Internal API interface +type ApplicationAPI interface { + List(context.Context, *request.ApplicationList) (interface{}, error) + Create(context.Context, *request.ApplicationCreate) (interface{}, error) + Update(context.Context, *request.ApplicationUpdate) (interface{}, error) + Read(context.Context, *request.ApplicationRead) (interface{}, error) + Delete(context.Context, *request.ApplicationDelete) (interface{}, error) +} + +// HTTP API interface +type Application struct { + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) +} + +func NewApplication(ah ApplicationAPI) *Application { + return &Application{ + List: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewApplicationList() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return ah.List(r.Context(), params) + }) + }, + Create: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewApplicationCreate() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return ah.Create(r.Context(), params) + }) + }, + Update: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewApplicationUpdate() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return ah.Update(r.Context(), params) + }) + }, + Read: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewApplicationRead() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return ah.Read(r.Context(), params) + }) + }, + Delete: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewApplicationDelete() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return ah.Delete(r.Context(), params) + }) + }, + } +} + +func (ah *Application) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Route("/application", func(r chi.Router) { + r.Get("/", ah.List) + r.Post("/", ah.Create) + r.Put("/{applicationID}", ah.Update) + r.Get("/{applicationID}", ah.Read) + r.Delete("/{applicationID}", ah.Delete) + }) + }) +} diff --git a/system/rest/request/application.go b/system/rest/request/application.go new file mode 100644 index 000000000..1b650ac99 --- /dev/null +++ b/system/rest/request/application.go @@ -0,0 +1,290 @@ +package request + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `application.go`, `application.util.go` or `application_test.go` to + implement your API calls, helper functions and tests. The file `application.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "encoding/json" + "io" + "mime/multipart" + "net/http" + "strings" + + "github.com/go-chi/chi" + "github.com/pkg/errors" + + sqlxTypes "github.com/jmoiron/sqlx/types" +) + +var _ = chi.URLParam +var _ = multipart.FileHeader{} + +// Application list request parameters +type ApplicationList struct { +} + +func NewApplicationList() *ApplicationList { + return &ApplicationList{} +} + +func (apReq *ApplicationList) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(apReq) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + return err +} + +var _ RequestFiller = NewApplicationList() + +// Application create request parameters +type ApplicationCreate struct { + Name string + Enabled bool + Unify sqlxTypes.JSONText + Config sqlxTypes.JSONText +} + +func NewApplicationCreate() *ApplicationCreate { + return &ApplicationCreate{} +} + +func (apReq *ApplicationCreate) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(apReq) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := post["name"]; ok { + + apReq.Name = val + } + if val, ok := post["enabled"]; ok { + + apReq.Enabled = parseBool(val) + } + if val, ok := post["unify"]; ok { + + if apReq.Unify, err = parseJSONTextWithErr(val); err != nil { + return err + } + } + if val, ok := post["config"]; ok { + + if apReq.Config, err = parseJSONTextWithErr(val); err != nil { + return err + } + } + + return err +} + +var _ RequestFiller = NewApplicationCreate() + +// Application update request parameters +type ApplicationUpdate struct { + ApplicationID uint64 `json:",string"` + Name string + Enabled bool + Unify sqlxTypes.JSONText + Config sqlxTypes.JSONText +} + +func NewApplicationUpdate() *ApplicationUpdate { + return &ApplicationUpdate{} +} + +func (apReq *ApplicationUpdate) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(apReq) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + apReq.ApplicationID = parseUInt64(chi.URLParam(r, "applicationID")) + if val, ok := post["name"]; ok { + + apReq.Name = val + } + if val, ok := post["enabled"]; ok { + + apReq.Enabled = parseBool(val) + } + if val, ok := post["unify"]; ok { + + if apReq.Unify, err = parseJSONTextWithErr(val); err != nil { + return err + } + } + if val, ok := post["config"]; ok { + + if apReq.Config, err = parseJSONTextWithErr(val); err != nil { + return err + } + } + + return err +} + +var _ RequestFiller = NewApplicationUpdate() + +// Application read request parameters +type ApplicationRead struct { + ApplicationID uint64 `json:",string"` +} + +func NewApplicationRead() *ApplicationRead { + return &ApplicationRead{} +} + +func (apReq *ApplicationRead) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(apReq) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + apReq.ApplicationID = parseUInt64(chi.URLParam(r, "applicationID")) + + return err +} + +var _ RequestFiller = NewApplicationRead() + +// Application delete request parameters +type ApplicationDelete struct { + ApplicationID uint64 `json:",string"` +} + +func NewApplicationDelete() *ApplicationDelete { + return &ApplicationDelete{} +} + +func (apReq *ApplicationDelete) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(apReq) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + apReq.ApplicationID = parseUInt64(chi.URLParam(r, "applicationID")) + + return err +} + +var _ RequestFiller = NewApplicationDelete() diff --git a/system/rest/router.go b/system/rest/router.go index d8529ebb3..f897e07c9 100644 --- a/system/rest/router.go +++ b/system/rest/router.go @@ -50,6 +50,7 @@ func MountRoutes(oidcConfig *config.OIDC, socialConfig *config.Social, jwtEncode handlers.NewRole(Role{}.New()).MountRoutes(r) handlers.NewOrganisation(Organisation{}.New()).MountRoutes(r) handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) + handlers.NewApplication(Application{}.New()).MountRoutes(r) }) } } diff --git a/system/service/application.go b/system/service/application.go new file mode 100644 index 000000000..6c2ea7fde --- /dev/null +++ b/system/service/application.go @@ -0,0 +1,90 @@ +package service + +import ( + "context" + + "github.com/titpetric/factory" + + "github.com/crusttech/crust/system/repository" + "github.com/crusttech/crust/system/types" +) + +type ( + application struct { + db *factory.DB + ctx context.Context + + application repository.ApplicationRepository + } + + ApplicationService interface { + With(ctx context.Context) ApplicationService + + FindByID(applicationID uint64) (*types.Application, error) + Find() (types.ApplicationSet, error) + + Create(application *types.Application) (*types.Application, error) + Update(application *types.Application) (*types.Application, error) + DeleteByID(id uint64) error + } +) + +func Application() ApplicationService { + return (&application{}).With(context.Background()) +} + +func (svc *application) With(ctx context.Context) ApplicationService { + db := repository.DB(ctx) + return &application{ + db: db, + ctx: ctx, + application: repository.Application(ctx, db), + } +} + +func (svc *application) FindByID(id uint64) (*types.Application, error) { + // @todo: permission check if current user has access to this application + return svc.application.FindByID(id) +} + +func (svc *application) Find() (types.ApplicationSet, error) { + // @todo: permission check to return only applications that current user has access to + return svc.application.Find() +} + +func (svc *application) Create(mod *types.Application) (*types.Application, error) { + // @todo: permission check if current user can add/edit application + + return svc.application.Create(mod) +} + +func (svc *application) Update(mod *types.Application) (t *types.Application, err error) { + // @todo: permission check if current user can add/edit application + // @todo: make sure archived & deleted entries can not be edited + + return t, svc.db.Transaction(func() (err error) { + if t, err = svc.application.FindByID(mod.ID); err != nil { + return + } + + // Assign changed values + t.Name = mod.Name + t.Enabled = mod.Enabled + t.Unify = mod.Unify + + if t, err = svc.application.Update(t); err != nil { + return err + } + + return nil + }) +} + +func (svc *application) DeleteByID(id uint64) error { + // @todo: make history unavailable + // @todo: notify users that application has been removed (remove from web UI) + // @todo: permissions check if current user can remove application + return svc.application.DeleteByID(id) +} + +var _ ApplicationService = &application{} diff --git a/system/service/service.go b/system/service/service.go index a8b34e8bd..deb70d75f 100644 --- a/system/service/service.go +++ b/system/service/service.go @@ -17,6 +17,7 @@ var ( DefaultRole RoleService DefaultPermissions PermissionsService DefaultOrganisation OrganisationService + DefaultApplication ApplicationService ) func Init() { @@ -26,5 +27,6 @@ func Init() { DefaultRole = Role() DefaultPermissions = Permissions() DefaultOrganisation = Organisation() + DefaultApplication = Application() }) } diff --git a/system/service/validation.go b/system/service/validation.go index 5b62230ce..8a7ac74ea 100644 --- a/system/service/validation.go +++ b/system/service/validation.go @@ -12,6 +12,7 @@ var ( "grant": true, "organisation.create": true, "role.create": true, + "application.create": true, }, "system:organisation": map[string]bool{ "access": true, @@ -22,6 +23,11 @@ var ( "delete": true, "members.manage": true, }, + "application:role": map[string]bool{ + "read": true, + "update": true, + "delete": true, + }, "messaging": map[string]bool{ "access": true, "grant": true, diff --git a/system/types/application.gen.go b/system/types/application.gen.go new file mode 100644 index 000000000..88b50f95f --- /dev/null +++ b/system/types/application.gen.go @@ -0,0 +1,67 @@ +package types + +// Hello! This file is auto-generated. + +type ( + + // ApplicationSet slice of Application + // + // This type is auto-generated. + ApplicationSet []*Application +) + +// Walk iterates through every slice item and calls w(Application) err +// +// This function is auto-generated. +func (set ApplicationSet) Walk(w func(*Application) error) (err error) { + for i := range set { + if err = w(set[i]); err != nil { + return + } + } + + return +} + +// Filter iterates through every slice item, calls f(Application) (bool, err) and return filtered slice +// +// This function is auto-generated. +func (set ApplicationSet) Filter(f func(*Application) (bool, error)) (out ApplicationSet, err error) { + var ok bool + out = ApplicationSet{} + for i := range set { + if ok, err = f(set[i]); err != nil { + return + } else if ok { + out = append(out, set[i]) + } + } + + return +} + +// FindByID finds items from slice by its ID property +// +// This function is auto-generated. +func (set ApplicationSet) FindByID(ID uint64) *Application { + for i := range set { + if set[i].ID == ID { + return set[i] + } + } + + return nil +} + +// IDs returns a slice of uint64s from all items in the set +// +// This function is auto-generated. +func (set ApplicationSet) IDs() (IDs []uint64) { + IDs = make([]uint64, len(set)) + + for i := range set { + IDs[i] = set[i].ID + } + + return +} diff --git a/system/types/applications.go b/system/types/applications.go new file mode 100644 index 000000000..f167565c3 --- /dev/null +++ b/system/types/applications.go @@ -0,0 +1,59 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "time" + + "github.com/pkg/errors" +) + +type ( + Application struct { + ID uint64 `json:"applicationID,string" db:"id"` + Name string `json:"name" db:"name"` + OwnerID uint64 `json:"ownerID" db:"rel_owner"` + Enabled bool `json:"enabled" db:"enabled"` + + Unify *ApplicationUnify `json:"unify,omitempty" db:"unify"` + + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"` + DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"` + } + + ApplicationUnify struct { + Name string `json:"name,omitempty"` + Listed bool `json:"listed"` + Icon string `json:"icon"` + Logo string `json:"logo"` + Url string `json:"url"` + Config string `json:"config"` + Order uint `json:"order"` + } +) + +func (u *Application) Valid() bool { + return u.ID > 0 && u.DeletedAt == nil +} + +func (u *Application) Identity() uint64 { + return u.ID +} + +func (au *ApplicationUnify) Scan(value interface{}) error { + switch value.(type) { + case nil: + au = nil + case []uint8: + if err := json.Unmarshal(value.([]byte), au); err != nil { + return errors.Wrapf(err, "Can not scan '%v' into ApplicationUnify", value) + } + } + + return nil +} + +func (au ApplicationUnify) Value() (driver.Value, error) { + return json.Marshal(au) +}