diff --git a/api/system/spec.json b/api/system/spec.json index da5b2fb04..4685cf329 100644 --- a/api/system/spec.json +++ b/api/system/spec.json @@ -1,811 +1,908 @@ [ - { - "title": "Authentication", - "path": "/auth", - "entrypoint": "auth", - "authentication": [], - "apis": [ + { + "title": "Authentication", + "path": "/auth", + "entrypoint": "auth", + "authentication": [], + "apis": [ + { + "name": "check", + "method": "GET", + "title": "Check JWT token", + "path": "/check", + "parameters": {} + }, + { + "name": "login", + "method": "POST", + "title": "Login user", + "path": "/login", + "parameters": { + "post": [ { - "name": "check", - "method": "GET", - "title": "Check JWT token", - "path": "/check", - "parameters": {} + "name": "username", + "type": "string", + "required": true, + "title": "Username" }, { - "name": "login", - "method": "POST", - "title": "Login user", - "path": "/login", - "parameters": { - "post": [ - { - "name": "username", - "type": "string", - "required": true, - "title": "Username" - }, - { - "name": "password", - "type": "string", - "required": true, - "title": "Password" - } - ] - } - }, - { - "name": "logout", - "method": "GET", - "title": "Delete JWT token (Sign Out)", - "path": "/logout", - "parameters": {} + "name": "password", + "type": "string", + "required": true, + "title": "Password" } + ] + } + }, + { + "name": "logout", + "method": "GET", + "title": "Delete JWT token (Sign Out)", + "path": "/logout", + "parameters": {} + } + ] + }, + { + "title": "Settings", + "path": "/settings", + "entrypoint": "settings", + "authentication": [], + "struct": [ + { + "imports": [ + "sqlxTypes github.com/jmoiron/sqlx/types" ] + } + ], + "apis": [ + { + "name": "list", + "method": "GET", + "title": "List settings", + "path": "/", + "parameters": { + "get": [ + { + "name": "prefix", + "type": "string", + "title": "Key prefix" + } + ] + } + }, + { + "name": "update", + "method": "PATCH", + "title": "Update settings", + "path": "/", + "parameters": { + "post": [ + { + "name": "values", + "type": "sqlxTypes.JSONText", + "title": "Array of new settings: `[{ name: ..., value: ... }]`. Omit value to remove setting", + "required": true + } + ] + } + }, + { + "name": "get", + "method": "GET", + "title": "Check JWT token", + "path": "/{key}", + "parameters": { + "path": [ + { + "name": "key", + "type": "string", + "title": "Setting key", + "required": true + } + ], + "get": [ + { + "name": "ownerID", + "type": "uint64", + "title": "Owner ID" + } + ] + } + }, + { + "name": "set", + "method": "PUT", + "title": "Set a value for a key", + "path": "/{key}", + "parameters": { + "path": [ + { + "name": "key", + "type": "string", + "title": "Setting key", + "required": true + } + ], + "post": [ + { + "name": "ownerID", + "type": "uint64", + "title": "Owner" + }, + { + "name": "value", + "type": "sqlxTypes.JSONText", + "required": true, + "title": "Setting value" + } + ] + } + } + ] + },{ + "title": "Organisations", + "description": "Organisations represent a top-level grouping entity. There may be many organisations defined in a single deployment.", + "path": "/organisations", + "entrypoint": "organisation", + "authentication": [ + "Client ID", + "Session ID" + ], + "apis": [ + { + "name": "list", + "method": "GET", + "title": "List organisations", + "path": "/", + "parameters": { + "get": [ + { + "type": "string", + "name": "query", + "required": false, + "title": "Search query" + } + ] + } }, { - "title": "Organisations", - "description": "Organisations represent a top-level grouping entity. There may be many organisations defined in a single deployment.", - "path": "/organisations", - "entrypoint": "organisation", - "authentication": [ - "Client ID", - "Session ID" - ], - "apis": [ - { - "name": "list", - "method": "GET", - "title": "List organisations", - "path": "/", - "parameters": { - "get": [ - { - "type": "string", - "name": "query", - "required": false, - "title": "Search query" - } - ] - } - }, - { - "name": "create", - "method": "POST", - "title": "Create organisation", - "path": "/", - "parameters": { - "post": [ - { - "type": "string", - "name": "name", - "required": true, - "title": "Organisation Name" - } - ] - } - }, - { - "name": "update", - "method": "PUT", - "title": "Update organisation details", - "path": "/{id}", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "id", - "required": false, - "title": "Organisation ID" - } - ], - "post": [ - { - "type": "string", - "name": "name", - "required": true, - "title": "Organisation Name" - } - ] - } - }, - { - "name": "delete", - "method": "DELETE", - "title": "Remove organisation", - "path": "/{id}", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "id", - "required": true, - "title": "Organisation ID" - } - ] - } - }, - { - "name": "read", - "method": "GET", - "title": "Read organisation details", - "path": "/{id}", - "parameters": { - "get": [ - { - "type": "uint64", - "name": "id", - "required": true, - "title": "Organisation ID" - } - ] - } - }, - { - "name": "archive", - "method": "POST", - "title": "Archive organisation", - "path": "/{id}/archive", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "id", - "required": true, - "title": "Organisation ID" - } - ] - } - } + "name": "create", + "method": "POST", + "title": "Create organisation", + "path": "/", + "parameters": { + "post": [ + { + "type": "string", + "name": "name", + "required": true, + "title": "Organisation Name" + } ] + } }, { - "title": "Roles", - "description": "An organisation may have many roles. Roles may have many channels available. Access to channels may be shared between roles.", - "path": "/roles", - "entrypoint": "role", - "authentication": [ - "Client ID", - "Session ID" + "name": "update", + "method": "PUT", + "title": "Update organisation details", + "path": "/{id}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "id", + "required": false, + "title": "Organisation ID" + } ], - "apis": [ - { - "name": "list", - "method": "GET", - "title": "List roles", - "path": "/", - "parameters": { - "get": [ - { - "type": "string", - "name": "query", - "required": false, - "title": "Search query" - } - ] - } - }, - { - "name": "create", - "method": "POST", - "title": "Update role details", - "path": "/", - "parameters": { - "post": [ - { - "type": "string", - "name": "name", - "required": true, - "title": "Name of Role" - }, - { - "type": "[]string", - "name": "members", - "required": false, - "title": "Role member IDs" - } - ] - } - }, - { - "name": "update", - "method": "PUT", - "title": "Update role details", - "path": "/{roleID}", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "roleID", - "required": true, - "title": "Role ID" - } - ], - "post": [ - { - "type": "string", - "name": "name", - "required": false, - "title": "Name of Role" - }, - { - "type": "[]string", - "name": "members", - "required": false, - "title": "Role member IDs" - } - ] - } - }, - { - "name": "read", - "method": "GET", - "title": "Read role details and memberships", - "path": "/{roleID}", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "roleID", - "required": true, - "title": "Role ID" - } - ] - } - }, - { - "name": "delete", - "method": "DELETE", - "title": "Remove role", - "path": "/{roleID}", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "roleID", - "required": true, - "title": "Role ID" - } - ] - } - }, - { - "name": "archive", - "method": "POST", - "title": "Archive role", - "path": "/{roleID}/archive", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "roleID", - "required": true, - "title": "Role ID" - } - ] - } - }, - { - "name": "move", - "method": "POST", - "title": "Move role to different organisation", - "path": "/{roleID}/move", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "roleID", - "required": true, - "title": "Role ID" - } - ], - "post": [ - { - "type": "uint64", - "name": "organisationID", - "required": true, - "title": "Role ID" - } - ] - } - }, - { - "name": "merge", - "method": "POST", - "title": "Merge one role into another", - "path": "/{roleID}/merge", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "roleID", - "required": true, - "title": "Source Role ID" - } - ], - "post": [ - { - "type": "uint64", - "name": "destination", - "required": true, - "title": "Destination Role ID" - } - ] - } - }, - { - "name": "memberList", - "method": "GET", - "title": "Returns all role members", - "path": "/{roleID}/members", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "roleID", - "required": true, - "title": "Source Role ID" - } - ] - } - }, - { - "name": "memberAdd", - "method": "POST", - "title": "Add member to a role", - "path": "/{roleID}/member/{userID}", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "roleID", - "required": true, - "title": "Source Role ID" - }, - { - "type": "uint64", - "name": "userID", - "required": true, - "title": "User ID" - } - ] - } - }, - { - "name": "memberRemove", - "method": "DELETE", - "title": "Remove member from a role", - "path": "/{roleID}/member/{userID}", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "roleID", - "required": true, - "title": "Source Role ID" - }, - { - "type": "uint64", - "name": "userID", - "required": true, - "title": "User ID" - } - ] - } - } + "post": [ + { + "type": "string", + "name": "name", + "required": true, + "title": "Organisation Name" + } ] + } }, { - "title": "Users", - "path": "/users", - "entrypoint": "user", - "authentication": [ - "Client ID", - "Session ID" - ], - "struct": [ - { - "imports": [ - "github.com/crusttech/crust/system/types" - ] - } - ], - "apis": [ - { - "name": "list", - "method": "GET", - "title": "Search users (Directory)", - "path": "/", - "parameters": { - "get": [ - { - "type": "string", - "name": "query", - "required": false, - "title": "Search query to match against users" - }, - { - "type": "string", - "name": "username", - "required": false, - "title": "Search username to match against users" - }, - { - "type": "string", - "name": "email", - "required": false, - "title": "Search email to match against users" - } - ] - } - }, - { - "name": "create", - "method": "POST", - "title": "Create user", - "path": "/", - "parameters": { - "post": [ - { - "name": "email", - "type": "string", - "required": true, - "title": "Email" - }, - { - "name": "name", - "type": "string", - "required": false, - "title": "Name" - }, - { - "name": "handle", - "type": "string", - "required": false, - "title": "Handle" - }, - { - "name": "kind", - "type": "types.UserKind", - "required": false, - "title": "Kind (normal, bot)" - } - ] - } - }, - { - "name": "update", - "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": "name", - "type": "string", - "required": true, - "title": "Name" - }, - { - "name": "handle", - "type": "string", - "required": false, - "title": "Handle" - }, - { - "name": "kind", - "type": "types.UserKind", - "required": false, - "title": "Kind (normal, bot)" - } - ] - } - }, - { - "name": "read", - "method": "GET", - "title": "Read user details and memberships", - "path": "/{userID}", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "userID", - "required": true, - "title": "User ID" - } - ] - } - }, - { - "name": "delete", - "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" - } - ] - } - } + "name": "delete", + "method": "DELETE", + "title": "Remove organisation", + "path": "/{id}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "id", + "required": true, + "title": "Organisation ID" + } ] + } }, { - "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" - } - ] - } - } + "name": "read", + "method": "GET", + "title": "Read organisation details", + "path": "/{id}", + "parameters": { + "get": [ + { + "type": "uint64", + "name": "id", + "required": true, + "title": "Organisation ID" + } ] + } }, { - "title": "Permissions", - "parameters": {}, - "entrypoint": "permissions", - "path": "/permissions", - "authentication": [ - "Client ID", - "Session ID" - ], - "struct": [ - { - "imports": [ - "github.com/crusttech/crust/internal/rules" - ] - } - ], - "apis": [ - { - "name": "list", - "path": "/", - "method": "GET", - "title": "Retrieve defined permissions", - "parameters": {} - }, - { - "name": "effective", - "path": "/effective", - "method": "GET", - "title": "Effective rules for current user", - "parameters": { - "get": [ - { - "name": "resource", - "type": "string", - "required": false, - "title": "Show only rules for a specific resource" - } - ] - } - }, - { - "name": "read", - "path": "/{roleID}/rules", - "method": "GET", - "title": "Retrieve role permissions", - "parameters": { - "path": [ - { - "name": "roleID", - "type": "uint64", - "required": true, - "title": "Role ID" - } - ] - } - }, - { - "name": "delete", - "path": "/{roleID}/rules", - "method": "DELETE", - "title": "Remove all defined role permissions", - "parameters": { - "path": [ - { - "name": "roleID", - "type": "uint64", - "required": true, - "title": "Role ID" - } - ] - } - }, - { - "name": "update", - "path": "/{roleID}/rules", - "method": "PATCH", - "title": "Update permission settings", - "parameters": { - "path": [ - { - "name": "roleID", - "type": "uint64", - "required": true, - "title": "Role ID" - } - ], - "post": [ - { - "name": "permissions", - "type": "[]rules.Rule", - "required": true, - "title": "List of permissions to set" - } - ] - } - } + "name": "archive", + "method": "POST", + "title": "Archive organisation", + "path": "/{id}/archive", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "id", + "required": true, + "title": "Organisation ID" + } ] + } } -] \ No newline at end of file + ] +}, + { + "title": "Roles", + "description": "An organisation may have many roles. Roles may have many channels available. Access to channels may be shared between roles.", + "path": "/roles", + "entrypoint": "role", + "authentication": [ + "Client ID", + "Session ID" + ], + "apis": [ + { + "name": "list", + "method": "GET", + "title": "List roles", + "path": "/", + "parameters": { + "get": [ + { + "type": "string", + "name": "query", + "required": false, + "title": "Search query" + } + ] + } + }, + { + "name": "create", + "method": "POST", + "title": "Update role details", + "path": "/", + "parameters": { + "post": [ + { + "type": "string", + "name": "name", + "required": true, + "title": "Name of Role" + }, + { + "type": "[]string", + "name": "members", + "required": false, + "title": "Role member IDs" + } + ] + } + }, + { + "name": "update", + "method": "PUT", + "title": "Update role details", + "path": "/{roleID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "roleID", + "required": true, + "title": "Role ID" + } + ], + "post": [ + { + "type": "string", + "name": "name", + "required": false, + "title": "Name of Role" + }, + { + "type": "[]string", + "name": "members", + "required": false, + "title": "Role member IDs" + } + ] + } + }, + { + "name": "read", + "method": "GET", + "title": "Read role details and memberships", + "path": "/{roleID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "roleID", + "required": true, + "title": "Role ID" + } + ] + } + }, + { + "name": "delete", + "method": "DELETE", + "title": "Remove role", + "path": "/{roleID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "roleID", + "required": true, + "title": "Role ID" + } + ] + } + }, + { + "name": "archive", + "method": "POST", + "title": "Archive role", + "path": "/{roleID}/archive", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "roleID", + "required": true, + "title": "Role ID" + } + ] + } + }, + { + "name": "move", + "method": "POST", + "title": "Move role to different organisation", + "path": "/{roleID}/move", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "roleID", + "required": true, + "title": "Role ID" + } + ], + "post": [ + { + "type": "uint64", + "name": "organisationID", + "required": true, + "title": "Role ID" + } + ] + } + }, + { + "name": "merge", + "method": "POST", + "title": "Merge one role into another", + "path": "/{roleID}/merge", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "roleID", + "required": true, + "title": "Source Role ID" + } + ], + "post": [ + { + "type": "uint64", + "name": "destination", + "required": true, + "title": "Destination Role ID" + } + ] + } + }, + { + "name": "memberList", + "method": "GET", + "title": "Returns all role members", + "path": "/{roleID}/members", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "roleID", + "required": true, + "title": "Source Role ID" + } + ] + } + }, + { + "name": "memberAdd", + "method": "POST", + "title": "Add member to a role", + "path": "/{roleID}/member/{userID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "roleID", + "required": true, + "title": "Source Role ID" + }, + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } + }, + { + "name": "memberRemove", + "method": "DELETE", + "title": "Remove member from a role", + "path": "/{roleID}/member/{userID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "roleID", + "required": true, + "title": "Source Role ID" + }, + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } + } + ] + }, + { + "title": "Users", + "path": "/users", + "entrypoint": "user", + "authentication": [ + "Client ID", + "Session ID" + ], + "struct": [ + { + "imports": [ + "github.com/crusttech/crust/system/types" + ] + } + ], + "apis": [ + { + "name": "list", + "method": "GET", + "title": "Search users (Directory)", + "path": "/", + "parameters": { + "get": [ + { + "type": "string", + "name": "query", + "required": false, + "title": "Search query to match against users" + }, + { + "type": "string", + "name": "username", + "required": false, + "title": "Search username to match against users" + }, + { + "type": "string", + "name": "email", + "required": false, + "title": "Search email to match against users" + } + ] + } + }, + { + "name": "create", + "method": "POST", + "title": "Create user", + "path": "/", + "parameters": { + "post": [ + { + "name": "email", + "type": "string", + "required": true, + "title": "Email" + }, + { + "name": "name", + "type": "string", + "required": false, + "title": "Name" + }, + { + "name": "handle", + "type": "string", + "required": false, + "title": "Handle" + }, + { + "name": "kind", + "type": "types.UserKind", + "required": false, + "title": "Kind (normal, bot)" + } + ] + } + }, + { + "name": "update", + "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": "name", + "type": "string", + "required": true, + "title": "Name" + }, + { + "name": "handle", + "type": "string", + "required": false, + "title": "Handle" + }, + { + "name": "kind", + "type": "types.UserKind", + "required": false, + "title": "Kind (normal, bot)" + } + ] + } + }, + { + "name": "read", + "method": "GET", + "title": "Read user details and memberships", + "path": "/{userID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } + }, + { + "name": "delete", + "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" + } + ] + } + } + ] + }, + { + "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": {}, + "entrypoint": "permissions", + "path": "/permissions", + "authentication": [ + "Client ID", + "Session ID" + ], + "struct": [ + { + "imports": [ + "github.com/crusttech/crust/internal/rules" + ] + } + ], + "apis": [ + { + "name": "list", + "path": "/", + "method": "GET", + "title": "Retrieve defined permissions", + "parameters": {} + }, + { + "name": "effective", + "path": "/effective", + "method": "GET", + "title": "Effective rules for current user", + "parameters": { + "get": [ + { + "name": "resource", + "type": "string", + "required": false, + "title": "Show only rules for a specific resource" + } + ] + } + }, + { + "name": "read", + "path": "/{roleID}/rules", + "method": "GET", + "title": "Retrieve role permissions", + "parameters": { + "path": [ + { + "name": "roleID", + "type": "uint64", + "required": true, + "title": "Role ID" + } + ] + } + }, + { + "name": "delete", + "path": "/{roleID}/rules", + "method": "DELETE", + "title": "Remove all defined role permissions", + "parameters": { + "path": [ + { + "name": "roleID", + "type": "uint64", + "required": true, + "title": "Role ID" + } + ] + } + }, + { + "name": "update", + "path": "/{roleID}/rules", + "method": "PATCH", + "title": "Update permission settings", + "parameters": { + "path": [ + { + "name": "roleID", + "type": "uint64", + "required": true, + "title": "Role ID" + } + ], + "post": [ + { + "name": "permissions", + "type": "[]rules.Rule", + "required": true, + "title": "List of permissions to set" + } + ] + } + } + ] + } +] diff --git a/api/system/spec/settings.json b/api/system/spec/settings.json new file mode 100644 index 000000000..116a47865 --- /dev/null +++ b/api/system/spec/settings.json @@ -0,0 +1,100 @@ +{ + "Title": "Settings", + "Interface": "Settings", + "Struct": [ + { + "imports": [ + "sqlxTypes github.com/jmoiron/sqlx/types" + ] + } + ], + "Parameters": null, + "Protocol": "", + "Authentication": [], + "Path": "/settings", + "APIs": [ + { + "Name": "list", + "Method": "GET", + "Title": "List settings", + "Path": "/", + "Parameters": { + "get": [ + { + "name": "prefix", + "title": "Key prefix", + "type": "string" + } + ] + } + }, + { + "Name": "update", + "Method": "PATCH", + "Title": "Update settings", + "Path": "/", + "Parameters": { + "post": [ + { + "name": "values", + "required": true, + "title": "Array of new settings: `[{ name: ..., value: ... }]`. Omit value to remove setting", + "type": "sqlxTypes.JSONText" + } + ] + } + }, + { + "Name": "get", + "Method": "GET", + "Title": "Check JWT token", + "Path": "/{key}", + "Parameters": { + "get": [ + { + "name": "ownerID", + "title": "Owner ID", + "type": "uint64" + } + ], + "path": [ + { + "name": "key", + "required": true, + "title": "Setting key", + "type": "string" + } + ] + } + }, + { + "Name": "set", + "Method": "PUT", + "Title": "Set a value for a key", + "Path": "/{key}", + "Parameters": { + "path": [ + { + "name": "key", + "required": true, + "title": "Setting key", + "type": "string" + } + ], + "post": [ + { + "name": "ownerID", + "title": "Owner", + "type": "uint64" + }, + { + "name": "value", + "required": true, + "title": "Setting value", + "type": "sqlxTypes.JSONText" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/codegen.sh b/codegen.sh index fa50de7e3..7bb557a5a 100755 --- a/codegen.sh +++ b/codegen.sh @@ -50,6 +50,8 @@ function types { ./build/gen-type-set --types Organisation --output system/types/organisation.gen.go ./build/gen-type-set --types Credentials --output system/types/credentials.gen.go + ./build/gen-type-set --types Value --output internal/settings/types.gen.go --with-primary-key=false --package settings + green "OK" } diff --git a/crm/db/mysql/statik.go b/crm/db/mysql/statik.go new file mode 100644 index 000000000..c8986b219 --- /dev/null +++ b/crm/db/mysql/statik.go @@ -0,0 +1,16 @@ +// Code generated by statik. DO NOT EDIT. + +// Package statik contains static assets. +package mysql + +import ( + "github.com/rakyll/statik/fs" +) + +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\x80Cm8CREATE TABLE `crm_content` (\n `id` bigint(20) unsigned NOT NULL,\n `module_id` bigint(20) unsigned NOT NULL,\n `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` datetime DEFAULT NULL,\n `deleted_at` datetime DEFAULT NULL,\n PRIMARY KEY (`id`,`module_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_content_column` (\n `content_id` bigint(20) NOT NULL,\n `column_name` varchar(255) NOT NULL,\n `column_value` text NOT NULL,\n PRIMARY KEY (`content_id`,`column_name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_field` (\n `field_type` varchar(16) NOT NULL COMMENT 'Short field type (string, boolean,...)',\n `field_name` varchar(255) NOT NULL COMMENT 'Description of field contents',\n `field_template` varchar(255) NOT NULL COMMENT 'HTML template file for field',\n PRIMARY KEY (`field_type`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_module` (\n `id` bigint(20) unsigned NOT NULL,\n `name` varchar(64) NOT NULL COMMENT 'The name of the module',\n `json` json NOT NULL COMMENT 'List of field definitions for the module',\n `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` datetime DEFAULT NULL,\n `deleted_at` datetime DEFAULT NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_module_form` (\n `module_id` bigint(20) unsigned NOT NULL,\n `place` tinyint(3) unsigned NOT NULL,\n `kind` varchar(64) NOT NULL COMMENT 'The type of the form input field',\n `name` varchar(64) NOT NULL COMMENT 'The name of the field in the form',\n `label` varchar(255) NOT NULL COMMENT 'The label of the form input',\n `help_text` text NOT NULL COMMENT 'Help text',\n `default_value` text NOT NULL COMMENT 'Default value',\n `max_length` int(10) unsigned NOT NULL COMMENT 'Maximum input length',\n `is_private` tinyint(1) NOT NULL COMMENT 'Contains personal/sensitive data?',\n PRIMARY KEY (`module_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_page` (\n `id` bigint(20) unsigned NOT NULL COMMENT 'Page ID',\n `self_id` bigint(20) unsigned NOT NULL COMMENT 'Parent Page ID',\n `module_id` bigint(20) unsigned NOT NULL COMMENT 'Module ID (optional)',\n `title` varchar(255) NOT NULL COMMENT 'Title (required)',\n `description` text NOT NULL COMMENT 'Description',\n `blocks` json NOT NULL COMMENT 'JSON array of blocks for the page',\n `visible` tinyint(4) NOT NULL COMMENT 'Is page visible in navigation?',\n `weight` int(11) NOT NULL COMMENT 'Order for navigation',\n PRIMARY KEY (`id`) USING BTREE,\n KEY `module_id` (`module_id`),\n KEY `self_id` (`self_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\xac\xe8\x19\x1d\x12\n\x00\x00\x12\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 \x0020180704080001.crm_fields-data.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `crm_field` VALUES ('bool','Boolean value (yes / no)','');\nINSERT INTO `crm_field` VALUES ('email','E-mail input','');\nINSERT INTO `crm_field` VALUES ('enum','Single option picker','');\nINSERT INTO `crm_field` VALUES ('hidden','Hidden value','');\nINSERT INTO `crm_field` VALUES ('stamp','Date/time input','');\nINSERT INTO `crm_field` VALUES ('text','Text input','');\nINSERT INTO `crm_field` VALUES ('textarea','Text input (multi-line)','');\nPK\x07\x08f\x18\x1e\x84\xc5\x01\x00\x00\xc5\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 \x0020181109133134.crm_content-ownership.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` ADD `user_id` BIGINT UNSIGNED NOT NULL AFTER `module_id`, ADD INDEX (`user_id`);\nPK\x07\x08\xeb!\x81\xc2k\x00\x00\x00k\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 \x0020181109193047.crm_fields-related_types.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `crm_field` (`field_type`, `field_name`, `field_template`) VALUES ('related', 'Related content', ''), ('related_multi', 'Related content (multiple)', '');PK\x07\x08:.\xfb8\xa6\x00\x00\x00\xa6\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x0020181125122152.add_multiple_relationships.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_content_links` (\n `content_id` bigint(20) unsigned NOT NULL,\n `column_name` varchar(255) NOT NULL,\n `rel_content_id` bigint(20) unsigned NOT NULL,\n PRIMARY KEY (`content_id`,`column_name`,`rel_content_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;PK\x07\x08\xee\x12\x15 \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\x00D\x00 \x0020181125132142.add_required_and_visible_to_module_form_fields.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` ADD `is_required` TINYINT(1) NOT NULL AFTER `is_private`, ADD `is_visible` TINYINT(1) NOT NULL AFTER `is_required`;PK\x07\x08\xa5q c\x91\x00\x00\x00\x91\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x005\x00 \x0020181202163130.fix-crm-module-form-primary-key.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` DROP PRIMARY KEY, ADD PRIMARY KEY(`module_id`, `place`);\nPK\x07\x08\xd9\xd4i\xe3W\x00\x00\x00W\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x0020181204123650.add-crm-content-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` ADD `json` json DEFAULT NULL COMMENT 'Content in JSON format.' AFTER `user_id`;\nPK\x07\x08\"\x96\xd6pj\x00\x00\x00j\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004\x00 \x0020181204155326.add-crm-module-form-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` ADD `json` JSON NOT NULL COMMENT 'Options in JSON format.' AFTER `kind`;PK\x07\x08\xb7\x93\xd4\xf6f\x00\x00\x00f\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 \x0020181216214630.crm-content-to-record.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` RENAME TO `crm_record`;\nALTER TABLE `crm_record` MODIFY COLUMN `json` json DEFAULT NULL COMMENT 'Records in JSON format.';\n\nALTER TABLE `crm_content_column` RENAME TO `crm_record_column`;\nALTER TABLE `crm_record_column` CHANGE COLUMN `content_id` `record_id` bigint(20);\n\nALTER TABLE `crm_content_links` RENAME TO `crm_record_links`;\nALTER TABLE `crm_record_links` CHANGE COLUMN `content_id` `record_id` bigint(20) unsigned;\nALTER TABLE `crm_record_links` CHANGE COLUMN `rel_content_id` `rel_record_id` bigint(20) unsigned;\nPK\x07\x08mA\xa8\x1e&\x02\x00\x00&\x02\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 \x0020181217100000.add-charts-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_chart` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'The name of the chart',\n `config` JSON NOT NULL COMMENT 'Chart & reporting configuration',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xcf\xc6g\xf6\xe4\x01\x00\x00\xe4\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 \x0020181224122301.rem-crm_field.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE `crm_field`;\nPK\x07\x08\xae \xfd2\x18\x00\x00\x00\x18\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 \x0020190108100000.add-triggers-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_trigger` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'The name of the trigger',\n `enabled` BOOLEAN NOT NULL COMMENT 'Trigger enabled?',\n `actions` TEXT NOT NULL COMMENT 'All actions that trigger it',\n `source` TEXT NOT NULL COMMENT 'Trigger source',\n `rel_module` BIGINT(20) UNSIGNED NULL COMMENT 'Primary module',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08+\xad\xb7\xed\xb8\x02\x00\x00\xb8\x02\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 \x0020190110175924.rem-crm-record-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_record` DROP COLUMN `json`;\nPK\x07\x08\x94#\xb9\x99-\x00\x00\x00-\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x008\x00 \x0020190114072000.cleanup-record-tables-and-multival.up.sqlUT\x05\x00\x01\x80Cm8-- No more links, we'll handle this through ref field on crm_record_value tbl\nDROP TABLE IF EXISTS `crm_record_links`;\n\n-- Not columns, values\nALTER TABLE `crm_record_column` RENAME TO `crm_record_value`;\n\n-- Simplify names\nALTER TABLE `crm_record_value` CHANGE COLUMN `column_name` `name` VARCHAR(64);\nALTER TABLE `crm_record_value` CHANGE COLUMN `column_value` `value` TEXT;\n\n-- Add reference\nALTER TABLE `crm_record_value` ADD COLUMN `ref` BIGINT UNSIGNED DEFAULT 0 NOT NULL;\nALTER TABLE `crm_record_value` ADD COLUMN `deleted_at` datetime DEFAULT NULL;\nALTER TABLE `crm_record_value` ADD COLUMN `place` INT UNSIGNED DEFAULT 0 NOT NULL;\nALTER TABLE `crm_record_value` DROP PRIMARY KEY, ADD PRIMARY KEY(`record_id`, `name`, `place`);\nCREATE INDEX crm_record_value_ref ON crm_record_value (ref);\n\n\n-- We want this as a real field\nALTER TABLE `crm_module_form` ADD COLUMN `is_multi` TINYINT(1) NOT NULL;\n\n-- This will be handled through meta(json) fieldd\nALTER TABLE `crm_module_form` DROP COLUMN `help_text`;\nALTER TABLE `crm_module_form` DROP COLUMN `max_length`;\nALTER TABLE `crm_module_form` DROP COLUMN `default_Value`;\nPK\x07\x08\x04]{\x1fo\x04\x00\x00o\x04\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 \x0020190121132408.record-updated-by.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_record` CHANGE COLUMN `user_id` `owned_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `deleted_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nUPDATE crm_record SET created_by = owned_by;\nUPDATE crm_record SET updated_by = owned_by WHERE updated_at IS NOT NULL;\nUPDATE crm_record SET deleted_by = owned_by WHERE deleted_at IS NOT NULL;\nPK\x07\x08h\xe2\xeb\n!\x02\x00\x00!\x02\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 \x0020190227090642.attachment.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE crm_attachment (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL,\n\n kind VARCHAR(32) NOT NULL,\n\n url VARCHAR(512),\n preview_url VARCHAR(512),\n\n size INT UNSIGNED,\n mimetype VARCHAR(255),\n name TEXT,\n\n meta JSON,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- page attachments will be referenced via page-block meta data\n-- module/record attachment will be referenced via crm_record_value\nPK\x07\x08\xce\xde?\x08\xb3\x02\x00\x00\xb3\x02\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\x089S\x05%x\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.sql\nPK\x07\x08\xc1h\xf1\xfb/\x00\x00\x00/\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xac\xe8\x19\x1d\x12\n\x00\x00\x12\n\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!(f\x18\x1e\x84\xc5\x01\x00\x00\xc5\x01\x00\x00%\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81c\n\x00\x0020180704080001.crm_fields-data.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xeb!\x81\xc2k\x00\x00\x00k\x00\x00\x00+\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x84\x0c\x00\x0020181109133134.crm_content-ownership.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(:.\xfb8\xa6\x00\x00\x00\xa6\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81Q\x0d\x00\x0020181109193047.crm_fields-related_types.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xee\x12\x15 \x05\x01\x00\x00\x05\x01\x00\x000\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\\\x0e\x00\x0020181125122152.add_multiple_relationships.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xa5q c\x91\x00\x00\x00\x91\x00\x00\x00D\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc8\x0f\x00\x0020181125132142.add_required_and_visible_to_module_form_fields.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xd9\xd4i\xe3W\x00\x00\x00W\x00\x00\x005\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd4\x10\x00\x0020181202163130.fix-crm-module-form-primary-key.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\"\x96\xd6pj\x00\x00\x00j\x00\x00\x000\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x97\x11\x00\x0020181204123650.add-crm-content-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xb7\x93\xd4\xf6f\x00\x00\x00f\x00\x00\x004\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81h\x12\x00\x0020181204155326.add-crm-module-form-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(mA\xa8\x1e&\x02\x00\x00&\x02\x00\x00+\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x13\x00\x0020181216214630.crm-content-to-record.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xcf\xc6g\xf6\xe4\x01\x00\x00\xe4\x01\x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc1\x15\x00\x0020181217100000.add-charts-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xae \xfd2\x18\x00\x00\x00\x18\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x18\x00\x0020181224122301.rem-crm_field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(+\xad\xb7\xed\xb8\x02\x00\x00\xb8\x02\x00\x00&\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81r\x18\x00\x0020190108100000.add-triggers-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x94#\xb9\x99-\x00\x00\x00-\x00\x00\x00/\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x87\x1b\x00\x0020190110175924.rem-crm-record-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x04]{\x1fo\x04\x00\x00o\x04\x00\x008\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x1a\x1c\x00\x0020190114072000.cleanup-record-tables-and-multival.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(h\xe2\xeb\n!\x02\x00\x00!\x02\x00\x00'\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xf8 \x00\x0020190121132408.record-updated-by.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xde?\x08\xb3\x02\x00\x00\xb3\x02\x00\x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81w#\x00\x0020190227090642.attachment.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9S\x05%x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x81&\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xc1h\xf1\xfb/\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\x13\x00\x13\x00\x14\x07\x00\x00\xaa(\x00\x00\x00\x00" +} + +func init() { + fs.Register(Data()) +} diff --git a/docs/system/README.md b/docs/system/README.md index d276cf6a2..806eeae12 100644 --- a/docs/system/README.md +++ b/docs/system/README.md @@ -515,6 +515,79 @@ An organisation may have many roles. Roles may have many channels available. Acc +# Settings + +| Method | Endpoint | Purpose | +| ------ | -------- | ------- | +| `GET` | `/settings/` | List settings | +| `PATCH` | `/settings/` | Update settings | +| `GET` | `/settings/{key}` | Check JWT token | +| `PUT` | `/settings/{key}` | Set a value for a key | + +## List settings + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/settings/` | HTTP/S | GET | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| prefix | string | GET | Key prefix | N/A | NO | + +## Update settings + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/settings/` | HTTP/S | PATCH | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| values | sqlxTypes.JSONText | POST | Array of new settings: `[{ name: ..., value: ... }]`. Omit value to remove setting | N/A | YES | + +## Check JWT token + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/settings/{key}` | HTTP/S | GET | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| ownerID | uint64 | GET | Owner ID | N/A | NO | +| key | string | PATH | Setting key | N/A | YES | + +## Set a value for a key + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/settings/{key}` | HTTP/S | PUT | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| key | string | PATH | Setting key | N/A | YES | +| ownerID | uint64 | POST | Owner | N/A | NO | +| value | sqlxTypes.JSONText | POST | Setting value | N/A | YES | + +--- + + + + # Users | Method | Endpoint | Purpose | diff --git a/internal/settings/repository.go b/internal/settings/repository.go new file mode 100644 index 000000000..3b0aa2ea4 --- /dev/null +++ b/internal/settings/repository.go @@ -0,0 +1,132 @@ +package settings + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" +) + +type ( + repository struct { + dbh *factory.DB + + // sql table reference + dbTable string + } + + Repository interface { + With(ctx context.Context) Repository + + Find(filter Filter) (ss ValueSet, err error) + + Get(name string, ownedBy uint64) (value *Value, err error) + Set(value *Value) error + BulkSet(vv ValueSet) error + Delete(name string, ownedBy uint64) (err error) + } +) + +func NewRepository(db *factory.DB, table string) Repository { + return &repository{ + dbTable: table, + dbh: db, + } +} + +func (r *repository) db() *factory.DB { + return r.dbh +} + +func (r repository) columns() []string { + return []string{ + "name", + "value", + "rel_owner", + "updated_at", + "updated_by", + } +} + +func (r *repository) With(ctx context.Context) Repository { + return &repository{ + dbTable: r.dbTable, + dbh: r.db().With(ctx), + } +} + +func (r *repository) Find(f Filter) (ss ValueSet, err error) { + f.Normalize() + lookup := squirrel. + Select(r.columns()...). + From(r.dbTable). + // Always filter by owner + Where("rel_owner = ?", f.OwnedBy) + + if len(f.Prefix) > 0 { + lookup = lookup.Where("name LIKE ?", f.Prefix+"%") + } + + if f.Page > 0 { + lookup = lookup.Offset(f.PerPage * f.Page) + } + + if f.PerPage > 0 { + lookup = lookup.Limit(f.PerPage) + } + + if query, args, err := lookup.ToSql(); err != nil { + return nil, errors.Wrap(err, "could not build lookup query for settings") + } else if err = r.db().Select(&ss, query, args...); err != nil { + return nil, errors.Wrap(err, "could not find settings") + } else { + return ss, nil + } +} + +func (r repository) BulkSet(vv ValueSet) error { + // Save all inside a db transaction + return r.db().Transaction(func() (err error) { + return vv.Walk(func(v *Value) error { + return r.Set(v) + }) + }) +} + +func (r *repository) Set(value *Value) error { + value.UpdatedAt = time.Now() + return r.db().Replace(r.dbTable, value) +} + +func (r *repository) Delete(name string, ownedBy uint64) error { + _, err := r.db().Exec( + fmt.Sprintf("DELETE FROM %s WHERE name = ? AND rel_owner = ? ", r.dbTable), + name, + ownedBy, + ) + return err +} + +func (r *repository) Get(name string, ownedBy uint64) (value *Value, err error) { + lookup := squirrel. + Select(r.columns()...). + From(r.dbTable). + Where("rel_owner = ?", ownedBy). + Where("name = ?", name) + + value = &Value{} + + if query, args, err := lookup.ToSql(); err != nil { + return nil, errors.Wrap(err, "could not build lookup query for settings") + } else if err = r.db().Get(value, query, args...); err != nil { + return nil, errors.Wrap(err, "could not get settings") + } else if value.Name == "" { + return nil, nil + } else { + return value, nil + } + +} diff --git a/internal/settings/settings.go b/internal/settings/settings.go new file mode 100644 index 000000000..e0657b02a --- /dev/null +++ b/internal/settings/settings.go @@ -0,0 +1,83 @@ +package settings + +import ( + "context" +) + +type ( + service struct { + repository Repository + } + + Service interface { + With(ctx context.Context) Service + + FindByPrefix(prefix string) (vv ValueSet, err error) + BulkSet(vv ValueSet) (err error) + Set(v *Value) (err error) + Get(name string, ownedBy uint64) (out *Value, err error) + Delete(name string, ownedBy uint64) error + GetGlobalString(name string) (out string, err error) + GetGlobalBool(name string) (out bool, err error) + } +) + +func NewService(r Repository) Service { + svc := &service{ + repository: r, + } + + return svc +} + +func (s service) With(ctx context.Context) Service { + return &service{ + repository: s.repository.With(ctx), + } +} + +func (s service) FindByPrefix(prefix string) (ValueSet, error) { + if vv, err := s.repository.Find(Filter{Prefix: prefix}); err != nil { + return nil, err + } else { + return vv, nil + } +} + +func (s service) Set(v *Value) (err error) { + return s.repository.Set(v) +} + +func (s service) BulkSet(vv ValueSet) (err error) { + return s.repository.BulkSet(vv) +} + +func (s service) Get(name string, ownedBy uint64) (out *Value, err error) { + return s.repository.Get(name, ownedBy) +} + +func (s service) Delete(name string, ownedBy uint64) error { + return s.repository.Delete(name, ownedBy) +} + +func (s service) GetGlobalString(name string) (out string, err error) { + const global = 0 + var v *Value + + if v, err = s.repository.Get(name, global); err == nil { + err = v.Value.Unmarshal(&out) + } + + return +} + +func (s service) GetGlobalBool(name string) (out bool, err error) { + const global = 0 + var v *Value + + if v, err = s.repository.Get(name, global); err == nil { + err = v.Value.Unmarshal(&out) + } + + return +} diff --git a/internal/settings/types.gen.go b/internal/settings/types.gen.go new file mode 100644 index 000000000..9984accc6 --- /dev/null +++ b/internal/settings/types.gen.go @@ -0,0 +1,41 @@ +package settings + +// Hello! This file is auto-generated. + +type ( + + // ValueSet slice of Value + // + // This type is auto-generated. + ValueSet []*Value +) + +// Walk iterates through every slice item and calls w(Value) err +// +// This function is auto-generated. +func (set ValueSet) Walk(w func(*Value) error) (err error) { + for i := range set { + if err = w(set[i]); err != nil { + return + } + } + + return +} + +// Filter iterates through every slice item, calls f(Value) (bool, err) and return filtered slice +// +// This function is auto-generated. +func (set ValueSet) Filter(f func(*Value) (bool, error)) (out ValueSet, err error) { + var ok bool + out = ValueSet{} + for i := range set { + if ok, err = f(set[i]); err != nil { + return + } else if ok { + out = append(out, set[i]) + } + } + + return +} diff --git a/internal/settings/types.go b/internal/settings/types.go new file mode 100644 index 000000000..838c83b47 --- /dev/null +++ b/internal/settings/types.go @@ -0,0 +1,104 @@ +package settings + +import ( + "encoding/json" + "strings" + "time" + + "github.com/jmoiron/sqlx/types" +) + +type ( + Value struct { + Name string `json:"name" db:"name"` + Value types.JSONText `json:"value" db:"value"` + + // Setting owner, 0 for global settings + OwnedBy uint64 `json:"-" db:"rel_owner"` + + // Who updated & when + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + UpdatedBy uint64 `json:"updatedBy" db:"updated_by"` + } + + Filter struct { + Prefix string `json:"prefix"` + OwnedBy uint64 `json:"ownedBy"` + Page uint64 `json:"page"` + PerPage uint64 `json:"perPage"` + } + + KV map[string]types.JSONText +) + +const ( + settingsFilterPerPageMax = 100 +) + +func (f *Filter) Normalize() { + f.Prefix = strings.TrimSpace(f.Prefix) + if f.PerPage > settingsFilterPerPageMax { + f.PerPage = settingsFilterPerPageMax + } +} + +func (v *Value) SetValueAsString(str string) error { + var dummy interface{} + // Test input to be sure we can save it... + if err := json.Unmarshal([]byte(str), &dummy); err != nil { + return err + } + + v.Value = types.JSONText(str) + return nil +} + +func (v *Value) SetValue(value interface{}) (err error) { + v.Value, err = json.Marshal(value) + return +} + +func (ss ValueSet) KV() KV { + m := KV{} + + _ = ss.Walk(func(v *Value) error { + m[v.Name] = v.Value + return nil + }) + + return m +} + +func (kv KV) Has(k string) (ok bool) { + _, ok = kv[k] + return +} + +func (kv KV) Bool(k string) (out bool) { + out = false + if v, ok := kv[k]; ok { + v.Unmarshal(&out) + } + + return +} + +func (kv KV) String(k string) (out string) { + out = "" + if v, ok := kv[k]; ok { + v.Unmarshal(&out) + } + + return +} + +func (kv KV) Filter(prefix string) KV { + var out = KV{} + for k, v := range kv { + if strings.Index(k, prefix) == 0 { + out[k] = v + } + } + + return out +} diff --git a/internal/settings/types_test.go b/internal/settings/types_test.go new file mode 100644 index 000000000..95398b175 --- /dev/null +++ b/internal/settings/types_test.go @@ -0,0 +1,62 @@ +package settings + +import ( + "testing" + + "github.com/jmoiron/sqlx/types" + + "github.com/crusttech/crust/internal/test" +) + +func TestKV_Bool(t *testing.T) { + type args struct { + k string + } + tests := []struct { + name string + kv KV + args args + wantV bool + }{ + { + name: "True value should return true", + kv: KV{"true-value": types.JSONText(`true`)}, + args: args{k: "true-value"}, + wantV: true, + }, + { + name: "Null value should return false", + kv: KV{"null-value": types.JSONText(`null`)}, + args: args{k: "null-value"}, + wantV: false, + }, + { + name: "Unexisting value should return false", + kv: KV{}, + args: args{k: "unexisting"}, + wantV: false, + }, + { + name: "Invalid KV should return false", + kv: nil, + args: args{k: "invalid-kv"}, + wantV: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotV := tt.kv.Bool(tt.args.k); gotV != tt.wantV { + t.Errorf("KV.Bool() = %v, want %v", gotV, tt.wantV) + } + }) + } +} + +func TestSettingValueAsString(t *testing.T) { + test.NoError(t, (&Value{}).SetValueAsString(`"string"`), "unable to set value as string") + test.NoError(t, (&Value{}).SetValueAsString(`false`), "unable to set value as string") + test.NoError(t, (&Value{}).SetValueAsString(`null`), "unable to set value as string") + test.NoError(t, (&Value{}).SetValueAsString(`42`), "unable to set value as string") + test.NoError(t, (&Value{}).SetValueAsString(`3.14`), "unable to set value as string") + test.Error(t, (&Value{}).SetValueAsString(`error`), "expecting error when not setting JSON") +} diff --git a/messaging/db/mysql/statik.go b/messaging/db/mysql/statik.go new file mode 100644 index 000000000..3b16d183d --- /dev/null +++ b/messaging/db/mysql/statik.go @@ -0,0 +1,16 @@ +// Code generated by statik. DO NOT EDIT. + +// Package statik contains static assets. +package mysql + +import ( + "github.com/rakyll/statik/fs" +) + +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-- Keeps all known channels\nCREATE TABLE channels (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the channel\n topic TEXT NOT NULL,\n meta JSON NOT NULL,\n\n type ENUM ('private', 'public', 'group') NOT NULL DEFAULT 'public',\n\n rel_organisation BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_creator BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- channel soft delete\n\n rel_last_message BIGINT UNSIGNED NOT NULL DEFAULT 0,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- handles channel membership\nCREATE TABLE channel_members (\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n type ENUM ('owner', 'member', 'invitee') NOT NULL DEFAULT 'member',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n\n PRIMARY KEY (rel_channel, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE channel_views (\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n -- timestamp of last view, should be enough to find out which messaghr\n viewed_at DATETIME NOT NULL DEFAULT NOW(),\n\n -- new messages count since last view\n new_since INT UNSIGNED NOT NULL DEFAULT 0,\n\n PRIMARY KEY (rel_user, rel_channel)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE channel_pins (\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (rel_channel, rel_message)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE messages (\n id BIGINT UNSIGNED NOT NULL,\n type TEXT,\n message TEXT NOT NULL,\n meta JSON,\n rel_user BIGINT UNSIGNED NOT NULL,\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n reply_to BIGINT UNSIGNED NULL REFERENCES messages(id),\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE reactions (\n id BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n reaction TEXT NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE attachments (\n id BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n\n url VARCHAR(512),\n preview_url VARCHAR(512),\n\n size INT UNSIGNED,\n mimetype VARCHAR(255),\n name TEXT,\n\n meta JSON,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE message_attachment (\n rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),\n rel_attachment BIGINT UNSIGNED NOT NULL REFERENCES attachment(id),\n\n PRIMARY KEY (rel_message)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE event_queue (\n id BIGINT UNSIGNED NOT NULL,\n origin BIGINT UNSIGNED NOT NULL,\n subscriber TEXT,\n payload JSON,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE event_queue_synced (\n origin BIGINT UNSIGNED NOT NULL,\n rel_last BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (origin)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xd5\x9c\xef\x89V\x10\x00\x00V\x10\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 \x0020181009080000.altering_types.up.sqlUT\x05\x00\x01\x80Cm8update channels set type = 'group' where type = 'direct';\nalter table channels CHANGE type type enum('private', 'public', 'group');\nalter table channel_members CHANGE type type enum('owner', 'member', 'invitee');\nPK\x07\x08E1\xf5\xa4\xd7\x00\x00\x00\xd7\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 \x0020181013080000.channel_views.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE channel_views DROP viewed_at;\nALTER TABLE channel_views ADD rel_last_message_id BIGINT UNSIGNED;\nALTER TABLE channel_views CHANGE new_since new_messages_count INT UNSIGNED;\n\n-- Table structure after these changes:\n-- +---------------------+---------------------+------+-----+---------+-------+\n-- | Field | Type | Null | Key | Default | Extra |\n-- +---------------------+---------------------+------+-----+---------+-------+\n-- | rel_channel | bigint(20) unsigned | NO | PRI | NULL | |\n-- | rel_user | bigint(20) unsigned | NO | PRI | NULL | |\n-- | rel_last_message_id | bigint(20) unsigned | YES | | NULL | |\n-- | new_messages_count | int(10) unsigned | NO | | 0 | |\n-- +---------------------+---------------------+------+-----+---------+-------+\n\n-- Prefill with data\nINSERT INTO channel_views (rel_channel, rel_user, rel_last_message_id)\n SELECT cm.rel_channel, cm.rel_user, max(m.ID)\n FROM channel_members AS cm INNER JOIN messages AS m ON (m.rel_channel = cm.rel_channel)\n GROUP BY cm.rel_channel, cm.rel_user;\n\nPK\x07\x08`\xcbP\xf9t\x04\x00\x00t\x04\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x00 \x0020181013080000.replies.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE messages CHANGE reply_to reply_to BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE messages ADD replies INT UNSIGNED NOT NULL DEFAULT 0;\nPK\x07\x08m\xedWA\x94\x00\x00\x00\x94\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 \x0020181101080000.pins_and_reactions.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE channel_pins;\nDROP TABLE reactions;\n\nCREATE TABLE message_flags (\n id BIGINT UNSIGNED NOT NULL,\n rel_channel BIGINT UNSIGNED NOT NULL,\n rel_message BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n flag TEXT,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08eA\x1eo\x90\x01\x00\x00\x90\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\x1e\x00 \x0020181107080000.mentions.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE mentions (\n id BIGINT UNSIGNED NOT NULL,\n rel_channel BIGINT UNSIGNED NOT NULL,\n rel_message BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n rel_mentioned_by BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX lookup_mentions ON mentions (rel_mentioned_by)\nPK\x07\x08\xfb\xe8\x9b\x98\xac\x01\x00\x00\xac\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\x1d\x00 \x0020181115080000.unreads.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE channel_views RENAME TO unreads;\n\nALTER TABLE unreads ADD rel_reply_to BIGINT UNSIGNED NOT NULL AFTER rel_channel;\nALTER TABLE unreads CHANGE rel_channel rel_channel BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE unreads CHANGE rel_user rel_user BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE unreads CHANGE rel_last_message_id rel_last_message BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE unreads CHANGE new_messages_count count INT UNSIGNED NOT NULL DEFAULT 0;\n\nPK\x07\x08jf1Q+\x02\x00\x00+\x02\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 \x0020181124173028.remove_events_tables.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE event_queue;\nDROP TABLE event_queue_synced;PK\x07\x08\xdd.y06\x00\x00\x006\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 \x0020181205153145.messages-to-utf8mb4.up.sqlUT\x05\x00\x01\x80Cm8alter table messages convert to character set utf8mb4 collate utf8mb4_unicode_ci;PK\x07\x08Ig\xbfOQ\x00\x00\x00Q\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 \x0020190122191150.membership-flags.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE channel_members ADD flag ENUM ('pinned', 'hidden', 'ignored', '') NOT NULL DEFAULT '' AFTER `type`;\nPK\x07\x084\xfb\xe3\xf4p\x00\x00\x00p\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 \x0020190206112022.prefix-tables.up.sqlUT\x05\x00\x01\x80Cm8-- misc tables\n\nALTER TABLE attachments RENAME TO messaging_attachment;\nALTER TABLE mentions RENAME TO messaging_mention;\nALTER TABLE unreads RENAME TO messaging_unread;\n\n-- channel tables\n\nALTER TABLE channels RENAME TO messaging_channel;\nALTER TABLE channel_members RENAME TO messaging_channel_member;\n\n-- message tables\n\nALTER TABLE messages RENAME TO messaging_message;\nALTER TABLE message_attachment RENAME TO messaging_message_attachment;\nALTER TABLE message_flags RENAME TO messaging_message_flag;\nPK\x07\x08\x145\xde}Q\x02\x00\x00Q\x02\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!(\xd5\x9c\xef\x89V\x10\x00\x00V\x10\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!(E1\xf5\xa4\xd7\x00\x00\x00\xd7\x00\x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xa7\x10\x00\x0020181009080000.altering_types.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(`\xcbP\xf9t\x04\x00\x00t\x04\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd9\x11\x00\x0020181013080000.channel_views.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(m\xedWA\x94\x00\x00\x00\x94\x00\x00\x00\x1d\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xa7\x16\x00\x0020181013080000.replies.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(eA\x1eo\x90\x01\x00\x00\x90\x01\x00\x00(\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x8f\x17\x00\x0020181101080000.pins_and_reactions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xfb\xe8\x9b\x98\xac\x01\x00\x00\xac\x01\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81~\x19\x00\x0020181107080000.mentions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(jf1Q+\x02\x00\x00+\x02\x00\x00\x1d\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x7f\x1b\x00\x0020181115080000.unreads.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xdd.y06\x00\x00\x006\x00\x00\x00*\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xfe\x1d\x00\x0020181124173028.remove_events_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(Ig\xbfOQ\x00\x00\x00Q\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x95\x1e\x00\x0020181205153145.messages-to-utf8mb4.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(4\xfb\xe3\xf4p\x00\x00\x00p\x00\x00\x00&\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81F\x1f\x00\x0020190122191150.membership-flags.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x145\xde}Q\x02\x00\x00Q\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x13 \x00\x0020190206112022.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\xbe\"\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\x0d\x00\x0d\x00\\\x04\x00\x00\xe6$\x00\x00\x00\x00" +} + +func init() { + fs.Register(Data()) +} diff --git a/system/cli/settings.go b/system/cli/settings.go new file mode 100644 index 000000000..661d6c426 --- /dev/null +++ b/system/cli/settings.go @@ -0,0 +1,88 @@ +package cli + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/crusttech/crust/internal/settings" +) + +func Settings(rootCmd *cobra.Command, service settings.Service) { + exit := func(err error) { + if err != nil { + rootCmd.Printf("Error: %v\n", err) + os.Exit(1) + } else { + os.Exit(0) + } + } + + settingsCmd := &cobra.Command{ + Use: "settings", + Short: "Settings management", + } + + list := &cobra.Command{ + Use: "list", + Short: "List all", + Run: func(cmd *cobra.Command, args []string) { + prefix := cmd.Flags().Lookup("prefix").Value.String() + if kv, err := service.FindByPrefix(prefix); err != nil { + exit(err) + } else { + for _, v := range kv { + cmd.Printf("%s\t%v\n", v.Name, v.Value) + } + } + }, + } + + list.Flags().String("prefix", "", "Filter settings by prefix") + + get := &cobra.Command{ + Use: "get [key to get]", + + Short: "Get value (raw JSON) for a specific key", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if v, err := service.Get(args[0], 0); err != nil { + exit(err) + } else if v != nil { + cmd.Printf("%v\n", v.Value) + } + exit(nil) + }, + } + + set := &cobra.Command{ + Use: "set [key to set] [value", + Short: "Set value (raw JSON) for a specific key", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + value := args[1] + v := &settings.Value{ + Name: args[0], + } + + if err := v.SetValueAsString(value); err != nil { + exit(err) + } + + exit(service.Set(v)) + }, + } + + del := &cobra.Command{ + Use: "delete [key to remove]", + Short: "Set value (raw JSON) for a specific key", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + exit(service.Delete(args[0], 0)) + }, + } + + settingsCmd.AddCommand(list, get, set, del) + + rootCmd.AddCommand(settingsCmd) +} diff --git a/system/db/mysql/statik.go b/system/db/mysql/statik.go new file mode 100644 index 000000000..858bfca8c --- /dev/null +++ b/system/db/mysql/statik.go @@ -0,0 +1,16 @@ +// Code generated by statik. DO NOT EDIT. + +// Package statik contains static assets. +package mysql + +import ( + "github.com/rakyll/statik/fs" +) + +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\x80Cm8REPLACE INTO `sys_role` (`id`, `name`, `handle`) VALUES\n (1, 'Everyone', 'everyone'),\n (2, 'Administrators', 'admins');\n\n-- Value: Allow (2), Deny (1), Inherit(0),\nREPLACE 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', 'access', 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.group.create', 2),\n (2, 'messaging:channel:*', 'update', 2),\n (2, 'messaging:channel:*', 'leave', 2),\n (2, 'messaging:channel:*', 'read', 2),\n (2, 'messaging:channel:*', 'join', 2),\n (2, 'messaging:channel:*', 'delete', 2),\n (2, 'messaging:channel:*', 'undelete', 2),\n (2, 'messaging:channel:*', 'archive', 2),\n (2, 'messaging:channel:*', 'unarchive', 2),\n (2, 'messaging:channel:*', 'members.manage', 2),\n (2, 'messaging:channel:*', 'webhooks.manage', 2),\n (2, 'messaging:channel:*', 'attachments.manage', 2),\n (2, 'messaging:channel:*', 'message.attach', 2),\n (2, 'messaging:channel:*', 'message.update.all', 2),\n (2, 'messaging:channel:*', 'message.update.own', 2),\n (2, 'messaging:channel:*', 'message.delete.all', 2),\n (2, 'messaging:channel:*', 'message.delete.own', 2),\n (2, 'messaging:channel:*', 'message.embed', 2),\n (2, 'messaging:channel:*', 'message.send', 2),\n (2, 'messaging:channel:*', 'message.reply', 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\x08z\",\xe8t\x0b\x00\x00t\x0b\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\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/\", \"listed\": true}'\n),\n( 2, 'Crust CRM', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/crm/\", \"listed\": true}'\n),\n( 3, 'Crust Admin Area', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/admin/\", \"listed\": true}'\n),\n( 4, 'Corteza Jitsi Bridge', true, 0,\n '{\"logo\": \"/applications/jitsi.png\", \"icon\": \"/applications/jitsi_icon.png\", \"url\": \"/bridge/jitsi/\", \"listed\": true}'\n),\n( 5, 'Google Maps', true, 0,\n '{\"logo\": \"/applications/google_maps.png\", \"icon\": \"/applications/google_maps_icon.png\", \"url\": \"/bridge/google-maps/\", \"listed\": true}'\n);\n\n-- Allow admin access to applications\nINSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n (1, 'system:application:*', 'read', 2),\n (2, 'system', 'application.create', 2),\n (2, 'system:application:*', 'read', 2),\n (2, 'system:application:*', 'update', 2),\n (2, 'system:application:*', 'delete', 2)\n;\nPK\x07\x08\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00 \x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE `settings`;\n\nCREATE TABLE `sys_settings` (\n rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings',\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value JSON COMMENT 'Setting value',\n\n updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated',\n updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value',\n\n PRIMARY KEY (name, rel_owner)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xba\xa4\xab\xaf\\\x02\x00\x00\\\x02\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!(z\",\xe8t\x0b\x00\x00t\x0b\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!(\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00\"\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81` \x00\x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xba\xa4\xab\xaf\\\x02\x00\x00\\\x02\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xca'\x00\x0020190326122000.settings.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{*\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\x818,\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x0d\x00\x0d\x00{\x04\x00\x00\xa3,\x00\x00\x00\x00" +} + +func init() { + fs.Register(Data()) +} diff --git a/system/db/schema/mysql/20190326122000.settings.up.sql b/system/db/schema/mysql/20190326122000.settings.up.sql new file mode 100644 index 000000000..f98ab175c --- /dev/null +++ b/system/db/schema/mysql/20190326122000.settings.up.sql @@ -0,0 +1,12 @@ +DROP TABLE `settings`; + +CREATE TABLE `sys_settings` ( + rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings', + name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys', + value JSON COMMENT 'Setting value', + + updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated', + updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value', + + PRIMARY KEY (name, rel_owner) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/system/internal/repository/role.go b/system/internal/repository/role.go index 2c9d35c73..7a3168052 100644 --- a/system/internal/repository/role.go +++ b/system/internal/repository/role.go @@ -188,6 +188,8 @@ func (r *role) Reset() error { (2, 'compose:page:*', 'delete', 2), (2, 'system', 'access', 2), (2, 'system', 'grant', 2), + (2, 'system', 'settings.read', 2), + (2, 'system', 'settings.manage', 2), (2, 'system', 'organisation.create', 2), (2, 'system', 'user.create', 2), (2, 'system', 'role.create', 2), diff --git a/system/internal/repository/settings.go b/system/internal/repository/settings.go deleted file mode 100644 index f408d3cb1..000000000 --- a/system/internal/repository/settings.go +++ /dev/null @@ -1,63 +0,0 @@ -package repository - -import ( - "context" - "encoding/json" - - "github.com/pkg/errors" - "github.com/titpetric/factory" -) - -type ( - settings struct { - *repository - - // sql table reference - settings string - } - - Settings interface { - With(ctx context.Context, db *factory.DB) Settings - - Get(name string, value interface{}) (bool, error) - Set(name string, value interface{}) error - } -) - -func NewSettings(ctx context.Context, db *factory.DB) Settings { - return (&settings{}).With(ctx, db) -} - -func (r *settings) With(ctx context.Context, db *factory.DB) Settings { - return &settings{ - repository: r.repository.With(ctx, db), - settings: "settings", - } -} - -func (r *settings) Set(name string, value interface{}) error { - if jsonValue, err := json.Marshal(value); err != nil { - return errors.Wrap(err, "Error marshaling settings value") - } else { - return r.db().Replace(r.settings, struct { - Key string `db:"name"` - Val json.RawMessage `db:"value"` - }{name, jsonValue}) - } -} - -func (r *settings) Get(name string, value interface{}) (bool, error) { - sql := "SELECT value FROM " + r.settings + " WHERE name = ?" - - var stored json.RawMessage - - if err := r.db().Get(&stored, sql, name); err != nil { - return false, errors.Wrap(err, "Error reading settings from the database") - } else if stored == nil { - return false, nil - } else if err := json.Unmarshal(stored, value); err != nil { - return false, errors.Wrap(err, "Error unmarshaling settings value") - } - - return true, nil -} diff --git a/system/internal/service/permissions.go b/system/internal/service/permissions.go index 11cf4b8bc..66889208c 100644 --- a/system/internal/service/permissions.go +++ b/system/internal/service/permissions.go @@ -25,6 +25,9 @@ type ( Effective() (ee []effectivePermission, err error) + CanReadSettings() bool + CanManageSettings() bool + CanCreateOrganisation() bool CanCreateUser() bool CanCreateRole() bool @@ -76,6 +79,8 @@ func (p *permissions) Effective() (ee []effectivePermission, err error) { } ee = append(ee, ep("system", "access", p.CanAccess())) + ee = append(ee, ep("system", "settings.read", p.CanReadSettings())) + ee = append(ee, ep("system", "settings.manage", p.CanManageSettings())) ee = append(ee, ep("system", "application.create", p.CanCreateApplication())) ee = append(ee, ep("system", "role.create", p.CanCreateRole())) ee = append(ee, ep("system", "organisation.create", p.CanCreateOrganisation())) @@ -88,6 +93,14 @@ func (p *permissions) CanAccess() bool { return p.checkAccess(types.PermissionResource, "access") } +func (p *permissions) CanReadSettings() bool { + return p.checkAccess(types.PermissionResource, "settings.read") +} + +func (p *permissions) CanManageSettings() bool { + return p.checkAccess(types.PermissionResource, "settings.manage") +} + func (p *permissions) CanCreateOrganisation() bool { return p.checkAccess(types.PermissionResource, "organisation.create") } diff --git a/system/internal/service/service.go b/system/internal/service/service.go index dd755e7a6..71ace34d9 100644 --- a/system/internal/service/service.go +++ b/system/internal/service/service.go @@ -11,6 +11,7 @@ type ( ) var ( + DefaultSettings SettingsService DefaultAuth AuthService DefaultUser UserService DefaultRole RoleService @@ -22,6 +23,7 @@ var ( func Init() error { ctx := context.Background() + DefaultSettings = Settings(ctx) DefaultRules = Rules(ctx) DefaultPermissions = Permissions(ctx) DefaultAuth = Auth(ctx) diff --git a/system/internal/service/settings.go b/system/internal/service/settings.go new file mode 100644 index 000000000..3f4317021 --- /dev/null +++ b/system/internal/service/settings.go @@ -0,0 +1,73 @@ +package service + +import ( + "context" + "errors" + + internalSettings "github.com/crusttech/crust/internal/settings" + "github.com/crusttech/crust/system/internal/repository" +) + +type ( + // Wrapper service for system around internal settings service + settings struct { + db db + ctx context.Context + + prm PermissionsService + internalSettings internalSettings.Service + } + + SettingsService interface { + With(ctx context.Context) SettingsService + FindByPrefix(prefix string) (vv internalSettings.ValueSet, err error) + Set(v *internalSettings.Value) (err error) + BulkSet(vv internalSettings.ValueSet) (err error) + Get(name string, ownedBy uint64) (out *internalSettings.Value, err error) + } +) + +func Settings(ctx context.Context) SettingsService { + return (&settings{}).With(ctx) +} + +func (svc settings) With(ctx context.Context) SettingsService { + db := repository.DB(ctx) + return &settings{ + ctx: ctx, + prm: Permissions(ctx), + internalSettings: internalSettings.NewService(internalSettings.NewRepository(db, "sys_settings")).With(ctx), + } +} + +func (svc settings) FindByPrefix(prefix string) (vv internalSettings.ValueSet, err error) { + if !svc.prm.CanReadSettings() { + return nil, errors.New("not allowed to read settings") + } + + return svc.internalSettings.FindByPrefix(prefix) +} + +func (svc settings) Set(v *internalSettings.Value) (err error) { + if !svc.prm.CanManageSettings() { + return errors.New("not allowed to manage settings") + } + + return svc.internalSettings.Set(v) +} + +func (svc settings) BulkSet(vv internalSettings.ValueSet) (err error) { + if !svc.prm.CanManageSettings() { + return errors.New("not allowed to manage settings") + } + + return svc.internalSettings.BulkSet(vv) +} + +func (svc settings) Get(name string, ownedBy uint64) (out *internalSettings.Value, err error) { + if !svc.prm.CanReadSettings() { + return nil, errors.New("not allowed to read settings") + } + + return svc.internalSettings.Get(name, ownedBy) +} diff --git a/system/internal/service/validation.go b/system/internal/service/validation.go index 9c426472b..ed11e1d10 100644 --- a/system/internal/service/validation.go +++ b/system/internal/service/validation.go @@ -11,6 +11,8 @@ var ( "system": map[string]bool{ "access": true, "grant": true, + "settings.read": true, + "settings.manage": true, "organisation.create": true, "role.create": true, "application.create": true, diff --git a/system/rest/handlers/settings.go b/system/rest/handlers/settings.go new file mode 100644 index 000000000..7cd7df87c --- /dev/null +++ b/system/rest/handlers/settings.go @@ -0,0 +1,86 @@ +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 `settings.go`, `settings.util.go` or `settings_test.go` to + implement your API calls, helper functions and tests. The file `settings.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "context" + + "net/http" + + "github.com/go-chi/chi" + "github.com/titpetric/factory/resputil" + + "github.com/crusttech/crust/system/rest/request" +) + +// Internal API interface +type SettingsAPI interface { + List(context.Context, *request.SettingsList) (interface{}, error) + Update(context.Context, *request.SettingsUpdate) (interface{}, error) + Get(context.Context, *request.SettingsGet) (interface{}, error) + Set(context.Context, *request.SettingsSet) (interface{}, error) +} + +// HTTP API interface +type Settings struct { + List func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Get func(http.ResponseWriter, *http.Request) + Set func(http.ResponseWriter, *http.Request) +} + +func NewSettings(sh SettingsAPI) *Settings { + return &Settings{ + List: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewSettingsList() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return sh.List(r.Context(), params) + }) + }, + Update: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewSettingsUpdate() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return sh.Update(r.Context(), params) + }) + }, + Get: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewSettingsGet() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return sh.Get(r.Context(), params) + }) + }, + Set: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewSettingsSet() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return sh.Set(r.Context(), params) + }) + }, + } +} + +func (sh *Settings) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Get("/settings/", sh.List) + r.Patch("/settings/", sh.Update) + r.Get("/settings/{key}", sh.Get) + r.Put("/settings/{key}", sh.Set) + }) +} diff --git a/system/rest/request/settings.go b/system/rest/request/settings.go new file mode 100644 index 000000000..0279bac54 --- /dev/null +++ b/system/rest/request/settings.go @@ -0,0 +1,230 @@ +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 `settings.go`, `settings.util.go` or `settings_test.go` to + implement your API calls, helper functions and tests. The file `settings.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "io" + "strings" + + "encoding/json" + "mime/multipart" + "net/http" + + "github.com/go-chi/chi" + "github.com/pkg/errors" + + sqlxTypes "github.com/jmoiron/sqlx/types" +) + +var _ = chi.URLParam +var _ = multipart.FileHeader{} + +// Settings list request parameters +type SettingsList struct { + Prefix string +} + +func NewSettingsList() *SettingsList { + return &SettingsList{} +} + +func (seReq *SettingsList) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(seReq) + + 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 := get["prefix"]; ok { + + seReq.Prefix = val + } + + return err +} + +var _ RequestFiller = NewSettingsList() + +// Settings update request parameters +type SettingsUpdate struct { + Values sqlxTypes.JSONText +} + +func NewSettingsUpdate() *SettingsUpdate { + return &SettingsUpdate{} +} + +func (seReq *SettingsUpdate) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(seReq) + + 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["values"]; ok { + + if seReq.Values, err = parseJSONTextWithErr(val); err != nil { + return err + } + } + + return err +} + +var _ RequestFiller = NewSettingsUpdate() + +// Settings get request parameters +type SettingsGet struct { + OwnerID uint64 `json:",string"` + Key string +} + +func NewSettingsGet() *SettingsGet { + return &SettingsGet{} +} + +func (seReq *SettingsGet) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(seReq) + + 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 := get["ownerID"]; ok { + + seReq.OwnerID = parseUInt64(val) + } + seReq.Key = chi.URLParam(r, "key") + + return err +} + +var _ RequestFiller = NewSettingsGet() + +// Settings set request parameters +type SettingsSet struct { + Key string + OwnerID uint64 `json:",string"` + Value sqlxTypes.JSONText +} + +func NewSettingsSet() *SettingsSet { + return &SettingsSet{} +} + +func (seReq *SettingsSet) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(seReq) + + 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]) + } + + seReq.Key = chi.URLParam(r, "key") + if val, ok := post["ownerID"]; ok { + + seReq.OwnerID = parseUInt64(val) + } + if val, ok := post["value"]; ok { + + if seReq.Value, err = parseJSONTextWithErr(val); err != nil { + return err + } + } + + return err +} + +var _ RequestFiller = NewSettingsSet() diff --git a/system/rest/router.go b/system/rest/router.go index 883f5eee9..0eba9eb96 100644 --- a/system/rest/router.go +++ b/system/rest/router.go @@ -51,6 +51,7 @@ func MountRoutes(oidcConfig *config.OIDC, socialConfig *config.Social, jwtEncode handlers.NewOrganisation(Organisation{}.New()).MountRoutes(r) handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) handlers.NewApplication(Application{}.New()).MountRoutes(r) + handlers.NewSettings(Settings{}.New()).MountRoutes(r) }) } } diff --git a/system/rest/settings.go b/system/rest/settings.go new file mode 100644 index 000000000..3d3713cec --- /dev/null +++ b/system/rest/settings.go @@ -0,0 +1,60 @@ +package rest + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/crusttech/crust/internal/settings" + "github.com/crusttech/crust/system/internal/service" + "github.com/crusttech/crust/system/rest/request" +) + +var _ = errors.Wrap + +type ( + Settings struct { + svc struct { + settings service.SettingsService + } + } +) + +func (Settings) New() *Settings { + ctrl := &Settings{} + ctrl.svc.settings = service.DefaultSettings + + return ctrl +} + +func (ctrl *Settings) List(ctx context.Context, r *request.SettingsList) (interface{}, error) { + if vv, err := ctrl.svc.settings.With(ctx).FindByPrefix(r.Prefix); err != nil { + return nil, err + } else { + return vv, err + } +} + +func (ctrl *Settings) Update(ctx context.Context, r *request.SettingsUpdate) (interface{}, error) { + values := settings.ValueSet{} + + if err := r.Values.Unmarshal(&values); err != nil { + return nil, err + } else if err := ctrl.svc.settings.With(ctx).BulkSet(values); err != nil { + return nil, err + } else { + return true, nil + } +} + +func (ctrl *Settings) Get(ctx context.Context, r *request.SettingsGet) (interface{}, error) { + if v, err := ctrl.svc.settings.With(ctx).Get(r.Key, r.OwnerID); err != nil { + return nil, err + } else { + return v, nil + } +} + +func (ctrl *Settings) Set(ctx context.Context, r *request.SettingsSet) (interface{}, error) { + return nil, errors.New("Not implemented: Settings.set") +} diff --git a/system/start.go b/system/start.go index 28d61afb6..0e554a2cb 100644 --- a/system/start.go +++ b/system/start.go @@ -14,7 +14,9 @@ import ( "github.com/crusttech/crust/internal/db" "github.com/crusttech/crust/internal/mail" "github.com/crusttech/crust/internal/metrics" + "github.com/crusttech/crust/internal/settings" migrate "github.com/crusttech/crust/system/db" + "github.com/crusttech/crust/system/internal/repository" "github.com/crusttech/crust/system/service" ) @@ -44,6 +46,17 @@ func Init(ctx context.Context) error { return err } + // Load settings from the database, + // for now, only at start-up time. + ctx := context.Background() + // settingsRepository := internalRepository.NewSettings(repository.DB(ctx), "sys_settings") + // if ss, err := settingsRepository.Find(types.SettingsFilter{}); err != nil { + // panic(err) + // } else { + // spew.Dump(ss.KV()) + // } + settingService := settings.NewService(settings.NewRepository(repository.DB(ctx), "sys_settings")) + // configure resputil options resputil.SetConfig(resputil.Options{ Pretty: flags.http.Pretty,