diff --git a/system/db/mysql/statik.go b/system/db/mysql/statik.go index 11f7ed300..7f33fa6bc 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\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!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd8\n\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\x95\x0c\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x04\x00\x04\x008\x01\x00\x00\x00\x0d\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\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\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81t\x0c\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\x811\x0e\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x05\x00\x05\x00\x9c\x01\x00\x00\x9c\x0e\x00\x00\x00\x00" } func init() { diff --git a/system/db/schema/mysql/20181125100429.add_user_kind_and_owner.up.sql b/system/db/schema/mysql/20181125100429.add_user_kind_and_owner.up.sql new file mode 100644 index 000000000..794d9f1c6 --- /dev/null +++ b/system/db/schema/mysql/20181125100429.add_user_kind_and_owner.up.sql @@ -0,0 +1,5 @@ +# add field to manage user type (bot support) +ALTER TABLE `sys_user` ADD `kind` VARCHAR(8) NOT NULL DEFAULT '' AFTER `handle`; + +# add field to manage "ownership" (get all bots created by user) +ALTER TABLE `sys_user` ADD `rel_user_id` BIGINT UNSIGNED NOT NULL AFTER `rel_organisation`, ADD INDEX (`rel_user_id`); diff --git a/system/docs/README.md b/system/docs/README.md index 44e48d802..bf44b65e1 100644 --- a/system/docs/README.md +++ b/system/docs/README.md @@ -283,10 +283,109 @@ An organisation may have many teams. Teams may have many channels available. Acc | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/users/search` | HTTP/S | GET | Client ID, Session ID | +| `/users/` | HTTP/S | GET | Client ID, Session ID | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | -| query | string | GET | Search query to match against users | N/A | NO | \ No newline at end of file +| query | string | GET | Search query to match against users | N/A | NO | + +## Create user + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/users/` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| email | string | POST | Email | N/A | YES | +| username | string | POST | Username | N/A | YES | +| password | string | POST | Password | N/A | YES | +| name | string | POST | Name | N/A | YES | +| handle | string | POST | Handle | N/A | YES | +| meta | types.JSONText | POST | Meta data | N/A | NO | +| satosaID | string | POST | Satosa ID | N/A | NO | +| organisationID | uint64 | POST | Organisation ID | N/A | NO | + +## Update user details + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/users/{userID}` | HTTP/S | PUT | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| userID | uint64 | PATH | User ID | N/A | YES | +| email | string | POST | Email | N/A | YES | +| username | string | POST | Username | N/A | YES | +| password | string | POST | Password | N/A | YES | +| name | string | POST | Name | N/A | YES | +| handle | string | POST | Handle | N/A | YES | +| meta | types.JSONText | POST | Meta data | N/A | NO | +| satosaID | string | POST | Satosa ID | N/A | NO | +| organisationID | uint64 | POST | Organisation ID | N/A | NO | + +## Read user details and memberships + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/users/{userID}` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| userID | uint64 | PATH | User ID | N/A | YES | + +## Remove user + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/users/{userID}` | HTTP/S | DELETE | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| userID | uint64 | PATH | User ID | N/A | YES | + +## Suspend user + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/users/{userID}/suspend` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| userID | uint64 | PATH | User ID | N/A | YES | + +## Unsuspend user + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/users/{userID}/unsuspend` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| userID | uint64 | PATH | User ID | N/A | YES | \ No newline at end of file diff --git a/system/docs/src/spec.json b/system/docs/src/spec.json index e9e1b329f..8f955e11e 100644 --- a/system/docs/src/spec.json +++ b/system/docs/src/spec.json @@ -373,11 +373,19 @@ "Client ID", "Session ID" ], + "struct": [ + { + "imports": [ + "github.com/jmoiron/sqlx/types" + ] + } + ], "apis": [ { - "name": "search", + "name": "list", "method": "GET", "title": "Search users (Directory)", + "path": "/", "parameters": { "get": [ { @@ -388,6 +396,194 @@ } ] } + }, + { + "name": "create", + "method": "POST", + "title": "Create user", + "path": "/", + "parameters": { + "post": [ + { + "name": "email", + "type": "string", + "required": true, + "title": "Email" + }, + { + "name": "username", + "type": "string", + "required": true, + "title": "Username" + }, + { + "name": "password", + "type": "string", + "required": true, + "title": "Password" + }, + { + "name": "name", + "type": "string", + "required": true, + "title": "Name" + }, + { + "name": "handle", + "type": "string", + "required": true, + "title": "Handle" + }, + { + "name": "meta", + "type": "types.JSONText", + "required": false, + "title": "Meta data" + }, + { + "name": "satosaID", + "type": "string", + "required": false, + "title": "Satosa ID" + }, + { + "name": "organisationID", + "type": "uint64", + "required": false, + "title": "Organisation ID" + } + ] + } + }, + { + "name": "edit", + "method": "PUT", + "title": "Update user details", + "path": "/{userID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ], + "post": [ + { + "name": "email", + "type": "string", + "required": true, + "title": "Email" + }, + { + "name": "username", + "type": "string", + "required": true, + "title": "Username" + }, + { + "name": "password", + "type": "string", + "required": true, + "title": "Password" + }, + { + "name": "name", + "type": "string", + "required": true, + "title": "Name" + }, + { + "name": "handle", + "type": "string", + "required": true, + "title": "Handle" + }, + { + "name": "meta", + "type": "types.JSONText", + "required": false, + "title": "Meta data" + }, + { + "name": "satosaID", + "type": "string", + "required": false, + "title": "Satosa ID" + }, + { + "name": "organisationID", + "type": "uint64", + "required": false, + "title": "Organisation ID" + } + ] + } + }, + { + "name": "read", + "method": "GET", + "title": "Read user details and memberships", + "path": "/{userID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } + }, + { + "name": "remove", + "method": "DELETE", + "title": "Remove user", + "path": "/{userID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } + }, + { + "name": "suspend", + "method": "POST", + "title": "Suspend user", + "path": "/{userID}/suspend", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } + }, + { + "name": "unsuspend", + "method": "POST", + "title": "Unsuspend user", + "path": "/{userID}/unsuspend", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } } ] } diff --git a/system/docs/src/spec/user.json b/system/docs/src/spec/user.json index bc08925bf..e98213109 100644 --- a/system/docs/src/spec/user.json +++ b/system/docs/src/spec/user.json @@ -2,7 +2,13 @@ "Title": "Users", "Package": "sam", "Interface": "User", - "Struct": null, + "Struct": [ + { + "imports": [ + "github.com/jmoiron/sqlx/types" + ] + } + ], "Parameters": null, "Protocol": "", "Authentication": [ @@ -12,10 +18,10 @@ "Path": "/users", "APIs": [ { - "Name": "search", + "Name": "list", "Method": "GET", "Title": "Search users (Directory)", - "Path": "/search", + "Path": "/", "Parameters": { "get": [ { @@ -26,6 +32,194 @@ } ] } + }, + { + "Name": "create", + "Method": "POST", + "Title": "Create user", + "Path": "/", + "Parameters": { + "post": [ + { + "name": "email", + "required": true, + "title": "Email", + "type": "string" + }, + { + "name": "username", + "required": true, + "title": "Username", + "type": "string" + }, + { + "name": "password", + "required": true, + "title": "Password", + "type": "string" + }, + { + "name": "name", + "required": true, + "title": "Name", + "type": "string" + }, + { + "name": "handle", + "required": true, + "title": "Handle", + "type": "string" + }, + { + "name": "meta", + "required": false, + "title": "Meta data", + "type": "types.JSONText" + }, + { + "name": "satosaID", + "required": false, + "title": "Satosa ID", + "type": "string" + }, + { + "name": "organisationID", + "required": false, + "title": "Organisation ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "edit", + "Method": "PUT", + "Title": "Update user details", + "Path": "/{userID}", + "Parameters": { + "path": [ + { + "name": "userID", + "required": true, + "title": "User ID", + "type": "uint64" + } + ], + "post": [ + { + "name": "email", + "required": true, + "title": "Email", + "type": "string" + }, + { + "name": "username", + "required": true, + "title": "Username", + "type": "string" + }, + { + "name": "password", + "required": true, + "title": "Password", + "type": "string" + }, + { + "name": "name", + "required": true, + "title": "Name", + "type": "string" + }, + { + "name": "handle", + "required": true, + "title": "Handle", + "type": "string" + }, + { + "name": "meta", + "required": false, + "title": "Meta data", + "type": "types.JSONText" + }, + { + "name": "satosaID", + "required": false, + "title": "Satosa ID", + "type": "string" + }, + { + "name": "organisationID", + "required": false, + "title": "Organisation ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "read", + "Method": "GET", + "Title": "Read user details and memberships", + "Path": "/{userID}", + "Parameters": { + "path": [ + { + "name": "userID", + "required": true, + "title": "User ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "remove", + "Method": "DELETE", + "Title": "Remove user", + "Path": "/{userID}", + "Parameters": { + "path": [ + { + "name": "userID", + "required": true, + "title": "User ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "suspend", + "Method": "POST", + "Title": "Suspend user", + "Path": "/{userID}/suspend", + "Parameters": { + "path": [ + { + "name": "userID", + "required": true, + "title": "User ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "unsuspend", + "Method": "POST", + "Title": "Unsuspend user", + "Path": "/{userID}/unsuspend", + "Parameters": { + "path": [ + { + "name": "userID", + "required": true, + "title": "User ID", + "type": "uint64" + } + ] + } } ] } \ No newline at end of file diff --git a/system/repository/user.go b/system/repository/user.go index 8122d96c3..c5951b962 100644 --- a/system/repository/user.go +++ b/system/repository/user.go @@ -21,6 +21,7 @@ type ( Create(mod *types.User) (*types.User, error) Update(mod *types.User) (*types.User, error) + SuspendByID(id uint64) error UnsuspendByID(id uint64) error DeleteByID(id uint64) error diff --git a/system/rest/handlers/user.go b/system/rest/handlers/user.go index ebd20089b..c95862a14 100644 --- a/system/rest/handlers/user.go +++ b/system/rest/handlers/user.go @@ -27,21 +27,75 @@ import ( // Internal API interface type UserAPI interface { - Search(context.Context, *request.UserSearch) (interface{}, error) + List(context.Context, *request.UserList) (interface{}, error) + Create(context.Context, *request.UserCreate) (interface{}, error) + Edit(context.Context, *request.UserEdit) (interface{}, error) + Read(context.Context, *request.UserRead) (interface{}, error) + Remove(context.Context, *request.UserRemove) (interface{}, error) + Suspend(context.Context, *request.UserSuspend) (interface{}, error) + Unsuspend(context.Context, *request.UserUnsuspend) (interface{}, error) } // HTTP API interface type User struct { - Search func(http.ResponseWriter, *http.Request) + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Edit func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Remove func(http.ResponseWriter, *http.Request) + Suspend func(http.ResponseWriter, *http.Request) + Unsuspend func(http.ResponseWriter, *http.Request) } func NewUser(uh UserAPI) *User { return &User{ - Search: func(w http.ResponseWriter, r *http.Request) { + List: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - params := request.NewUserSearch() + params := request.NewUserList() resputil.JSON(w, params.Fill(r), func() (interface{}, error) { - return uh.Search(r.Context(), params) + return uh.List(r.Context(), params) + }) + }, + Create: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewUserCreate() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return uh.Create(r.Context(), params) + }) + }, + Edit: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewUserEdit() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return uh.Edit(r.Context(), params) + }) + }, + Read: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewUserRead() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return uh.Read(r.Context(), params) + }) + }, + Remove: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewUserRemove() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return uh.Remove(r.Context(), params) + }) + }, + Suspend: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewUserSuspend() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return uh.Suspend(r.Context(), params) + }) + }, + Unsuspend: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewUserUnsuspend() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return uh.Unsuspend(r.Context(), params) }) }, } @@ -51,7 +105,13 @@ func (uh *User) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http r.Group(func(r chi.Router) { r.Use(middlewares...) r.Route("/users", func(r chi.Router) { - r.Get("/search", uh.Search) + r.Get("/", uh.List) + r.Post("/", uh.Create) + r.Put("/{userID}", uh.Edit) + r.Get("/{userID}", uh.Read) + r.Delete("/{userID}", uh.Remove) + r.Post("/{userID}/suspend", uh.Suspend) + r.Post("/{userID}/unsuspend", uh.Unsuspend) }) }) } diff --git a/system/rest/request/user.go b/system/rest/request/user.go index 3360e0607..3cdbd7073 100644 --- a/system/rest/request/user.go +++ b/system/rest/request/user.go @@ -30,16 +30,16 @@ var _ = chi.URLParam var _ = types.JSONText{} var _ = multipart.FileHeader{} -// User search request parameters -type UserSearch struct { +// User list request parameters +type UserList struct { Query string } -func NewUserSearch() *UserSearch { - return &UserSearch{} +func NewUserList() *UserList { + return &UserList{} } -func (u *UserSearch) Fill(r *http.Request) (err error) { +func (u *UserList) Fill(r *http.Request) (err error) { if strings.ToLower(r.Header.Get("content-type")) == "application/json" { err = json.NewDecoder(r.Body).Decode(u) @@ -74,4 +74,344 @@ func (u *UserSearch) Fill(r *http.Request) (err error) { return err } -var _ RequestFiller = NewUserSearch() +var _ RequestFiller = NewUserList() + +// User create request parameters +type UserCreate struct { + Email string + Username string + Password string + Name string + Handle string + Meta types.JSONText + SatosaID string + OrganisationID uint64 `json:",string"` +} + +func NewUserCreate() *UserCreate { + return &UserCreate{} +} + +func (u *UserCreate) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(u) + + 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["email"]; ok { + + u.Email = val + } + if val, ok := post["username"]; ok { + + u.Username = val + } + if val, ok := post["password"]; ok { + + u.Password = val + } + if val, ok := post["name"]; ok { + + u.Name = val + } + if val, ok := post["handle"]; ok { + + u.Handle = val + } + if val, ok := post["meta"]; ok { + + if u.Meta, err = parseJSONText(val); err != nil { + return err + } + } + if val, ok := post["satosaID"]; ok { + + u.SatosaID = val + } + if val, ok := post["organisationID"]; ok { + + u.OrganisationID = parseUInt64(val) + } + + return err +} + +var _ RequestFiller = NewUserCreate() + +// User edit request parameters +type UserEdit struct { + UserID uint64 `json:",string"` + Email string + Username string + Password string + Name string + Handle string + Meta types.JSONText + SatosaID string + OrganisationID uint64 `json:",string"` +} + +func NewUserEdit() *UserEdit { + return &UserEdit{} +} + +func (u *UserEdit) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(u) + + 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]) + } + + u.UserID = parseUInt64(chi.URLParam(r, "userID")) + if val, ok := post["email"]; ok { + + u.Email = val + } + if val, ok := post["username"]; ok { + + u.Username = val + } + if val, ok := post["password"]; ok { + + u.Password = val + } + if val, ok := post["name"]; ok { + + u.Name = val + } + if val, ok := post["handle"]; ok { + + u.Handle = val + } + if val, ok := post["meta"]; ok { + + if u.Meta, err = parseJSONText(val); err != nil { + return err + } + } + if val, ok := post["satosaID"]; ok { + + u.SatosaID = val + } + if val, ok := post["organisationID"]; ok { + + u.OrganisationID = parseUInt64(val) + } + + return err +} + +var _ RequestFiller = NewUserEdit() + +// User read request parameters +type UserRead struct { + UserID uint64 `json:",string"` +} + +func NewUserRead() *UserRead { + return &UserRead{} +} + +func (u *UserRead) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(u) + + 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]) + } + + u.UserID = parseUInt64(chi.URLParam(r, "userID")) + + return err +} + +var _ RequestFiller = NewUserRead() + +// User remove request parameters +type UserRemove struct { + UserID uint64 `json:",string"` +} + +func NewUserRemove() *UserRemove { + return &UserRemove{} +} + +func (u *UserRemove) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(u) + + 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]) + } + + u.UserID = parseUInt64(chi.URLParam(r, "userID")) + + return err +} + +var _ RequestFiller = NewUserRemove() + +// User suspend request parameters +type UserSuspend struct { + UserID uint64 `json:",string"` +} + +func NewUserSuspend() *UserSuspend { + return &UserSuspend{} +} + +func (u *UserSuspend) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(u) + + 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]) + } + + u.UserID = parseUInt64(chi.URLParam(r, "userID")) + + return err +} + +var _ RequestFiller = NewUserSuspend() + +// User unsuspend request parameters +type UserUnsuspend struct { + UserID uint64 `json:",string"` +} + +func NewUserUnsuspend() *UserUnsuspend { + return &UserUnsuspend{} +} + +func (u *UserUnsuspend) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(u) + + 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]) + } + + u.UserID = parseUInt64(chi.URLParam(r, "userID")) + + return err +} + +var _ RequestFiller = NewUserUnsuspend() diff --git a/system/rest/user.go b/system/rest/user.go index dff10cf3e..e0384d8d1 100644 --- a/system/rest/user.go +++ b/system/rest/user.go @@ -25,6 +25,55 @@ func (User) New() *User { } // Searches the users table in the database to find users by matching (by-prefix) their.Username -func (ctrl *User) Search(ctx context.Context, r *request.UserSearch) (interface{}, error) { +func (ctrl *User) List(ctx context.Context, r *request.UserList) (interface{}, error) { return ctrl.user.With(ctx).Find(&types.UserFilter{Query: r.Query}) } + +func (ctrl *User) Create(ctx context.Context, r *request.UserCreate) (interface{}, error) { + user := &types.User{ + Email: r.Email, + Username: r.Username, + Name: r.Name, + Handle: r.Handle, + Meta: r.Meta, + SatosaID: r.SatosaID, + OrganisationID: r.OrganisationID, + } + if err := user.GeneratePassword(r.Password); err != nil { + return nil, err + } + return ctrl.user.With(ctx).Create(user) +} + +func (ctrl *User) Edit(ctx context.Context, r *request.UserEdit) (interface{}, error) { + user := &types.User{ + ID: r.UserID, + Email: r.Email, + Username: r.Username, + Name: r.Name, + Handle: r.Handle, + Meta: r.Meta, + SatosaID: r.SatosaID, + OrganisationID: r.OrganisationID, + } + if err := user.GeneratePassword(r.Password); err != nil { + return nil, err + } + return ctrl.user.With(ctx).Update(user) +} + +func (ctrl *User) Read(ctx context.Context, r *request.UserRead) (interface{}, error) { + return ctrl.user.With(ctx).FindByID(r.UserID) +} + +func (ctrl *User) Remove(ctx context.Context, r *request.UserRemove) (interface{}, error) { + return nil, ctrl.user.With(ctx).Delete(r.UserID) +} + +func (ctrl *User) Suspend(ctx context.Context, r *request.UserSuspend) (interface{}, error) { + return nil, ctrl.user.With(ctx).Suspend(r.UserID) +} + +func (ctrl *User) Unsuspend(ctx context.Context, r *request.UserUnsuspend) (interface{}, error) { + return nil, ctrl.user.With(ctx).Unsuspend(r.UserID) +} diff --git a/system/service/user.go b/system/service/user.go index f236c7175..338f144a2 100644 --- a/system/service/user.go +++ b/system/service/user.go @@ -34,6 +34,10 @@ type ( Create(input *types.User) (*types.User, error) Update(mod *types.User) (*types.User, error) + Delete(id uint64) error + Suspend(id uint64) error + Unsuspend(id uint64) error + FindOrCreate(*types.User) (*types.User, error) ValidateCredentials(username, password string) (*types.User, error) } @@ -53,6 +57,18 @@ func (svc *user) With(ctx context.Context) UserService { } } +func (svc *user) Delete(id uint64) error { + return svc.user.DeleteByID(id) +} + +func (svc *user) Suspend(id uint64) error { + return svc.user.SuspendByID(id) +} + +func (svc *user) Unsuspend(id uint64) error { + return svc.user.UnsuspendByID(id) +} + func (svc *user) ValidateCredentials(username, password string) (*types.User, error) { user, err := svc.user.FindByUsername(username) if err != nil { diff --git a/system/types/user.go b/system/types/user.go index 0c9067dcf..c703b2e0d 100644 --- a/system/types/user.go +++ b/system/types/user.go @@ -9,15 +9,19 @@ import ( type ( User struct { - ID uint64 `json:"id" db:"id"` - Username string `json:"username" db:"username"` - Email string `json:"email" db:"email"` - Name string `json:"name" db:"name"` - Handle string `json:"handle" db:"handle"` - SatosaID string `json:"-" db:"satosa_id"` - Meta types.JSONText `json:"-" db:"meta"` - OrganisationID uint64 `json:"organisationId" db:"rel_organisation"` - Password []byte `json:"-" db:"password"` + ID uint64 `json:"id,string" db:"id"` + Username string `json:"username" db:"username"` + Email string `json:"email" db:"email"` + Name string `json:"name" db:"name"` + Handle string `json:"handle" db:"handle"` + SatosaID string `json:"-" db:"satosa_id"` + Meta types.JSONText `json:"-" db:"meta"` + + OrganisationID uint64 `json:"organisationID,string" db:"rel_organisation"` + UserID uint64 `json:"userID,string" db:"rel_user_id"` + User *User `json:"user" db:"-"` + + Password []byte `json:"-" db:"password"` CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"`