diff --git a/app/assets/app.css b/app/assets/app.css new file mode 100644 index 0000000..b9c4069 --- /dev/null +++ b/app/assets/app.css @@ -0,0 +1,14 @@ +#nav { + margin-bottom: 30px; + display: flex; + justify-content: space-between; + align-items: center; +} + +#nav ul { + display: flex; + justify-content: end; + align-items: center; + column-gap: 20px; + font-size: 16px; +} diff --git a/app/config.yml b/app/config.yml index e69de29..12bcb9f 100644 --- a/app/config.yml +++ b/app/config.yml @@ -0,0 +1,3 @@ +--- +escape_output_instead_of_sanitize: true +--- diff --git a/app/graphql/contacts/delete.graphql b/app/graphql/contacts/delete.graphql new file mode 100644 index 0000000..d957ba7 --- /dev/null +++ b/app/graphql/contacts/delete.graphql @@ -0,0 +1,6 @@ +mutation delete($id: ID!) { + record_delete( + table: "contact" + id: $id + ){ id } +} diff --git a/app/graphql/contacts/search.graphql b/app/graphql/contacts/search.graphql new file mode 100644 index 0000000..21f2263 --- /dev/null +++ b/app/graphql/contacts/search.graphql @@ -0,0 +1,25 @@ +query contacts_search($page: Int = 1, $per_page: Int = 20, $keyword: String) { + records( + page: $page + per_page: $per_page + filter: { + table: { value: "contact" } + or: [ + { properties: [ { name: "email", contains: $keyword } ] } + { properties: [ { name: "body", contains: $keyword } ] } + ] + } + sort: [{ + id: { order: DESC } + }] + ) { + total_pages + results { + id + body: property(name: "body") + email: property(name: "email") + created_at + } + } +} + diff --git a/app/lib/commands/contacts/create.liquid b/app/lib/commands/contacts/create.liquid index d84639b..e747c9d 100644 --- a/app/lib/commands/contacts/create.liquid +++ b/app/lib/commands/contacts/create.liquid @@ -6,7 +6,6 @@ function object = 'commands/contacts/create/execute', object: object assign event_object = '{}' | parse_json | hash_merge: id: object.id hash_assign event_object["email"] = object.email - log event_object, type: 'event object' function _ = 'modules/core/commands/events/publish', type: 'contact_created', object: event_object, delay: null, max_attempts: null endif diff --git a/app/lib/commands/contacts/delete.liquid b/app/lib/commands/contacts/delete.liquid new file mode 100644 index 0000000..e84ad84 --- /dev/null +++ b/app/lib/commands/contacts/delete.liquid @@ -0,0 +1,9 @@ +{% liquid + graphql r = 'contacts/delete', id: id + assign object = r.record_delete + + assign event_object = '{}' | parse_json | hash_merge: id: id + function _ = 'modules/core/commands/events/publish', type: 'contact_deleted', object: event_object, delay: null, max_attempts: null + + return object +%} diff --git a/app/lib/events/contact_deleted.liquid b/app/lib/events/contact_deleted.liquid new file mode 100644 index 0000000..0910a68 --- /dev/null +++ b/app/lib/events/contact_deleted.liquid @@ -0,0 +1,12 @@ +--- +metadata: + event: + id +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'id' + + return c +%} diff --git a/app/lib/queries/contacts/search.liquid b/app/lib/queries/contacts/search.liquid new file mode 100644 index 0000000..a4bc355 --- /dev/null +++ b/app/lib/queries/contacts/search.liquid @@ -0,0 +1,9 @@ +{% liquid + assign limit = limit | default: 20 + assign page = params.page | to_positive_integer: 1 + assign keyword = params.keyword | default: null + + graphql res = 'contacts/search', per_page: limit, page: page, keyword: keyword + + return res.records +%} diff --git a/app/modules/user/public/lib/queries/role_permissions/permissions.liquid b/app/modules/user/public/lib/queries/role_permissions/permissions.liquid new file mode 100644 index 0000000..e49d85e --- /dev/null +++ b/app/modules/user/public/lib/queries/role_permissions/permissions.liquid @@ -0,0 +1,11 @@ +{% parse_json data %} +{ + "anonymous": ["sessions.create", "users.register"], + "authenticated": ["sessions.destroy","oauth.manage"], + "admin": ["admin_pages.view", "contacts.manage", "admin.users.manage", "users.impersonate"], + "member": ["profile.manage"], + "superadmin": ["users.impersonate_superadmin"] +} +{% endparse_json %} + +{% return data %} diff --git a/app/pos-modules.json b/app/pos-modules.json index 36b45d7..4d870f2 100644 --- a/app/pos-modules.json +++ b/app/pos-modules.json @@ -1,6 +1,7 @@ { "modules": { - "core": "1.5.2", - "common-styling": "1.29.0" + "core": "2.0.7", + "common-styling": "1.32.0", + "user": "5.1.1" } } \ No newline at end of file diff --git a/app/pos-modules.lock.json b/app/pos-modules.lock.json index 36b45d7..4d870f2 100644 --- a/app/pos-modules.lock.json +++ b/app/pos-modules.lock.json @@ -1,6 +1,7 @@ { "modules": { - "core": "1.5.2", - "common-styling": "1.29.0" + "core": "2.0.7", + "common-styling": "1.32.0", + "user": "5.1.1" } } \ No newline at end of file diff --git a/app/views/layouts/application.liquid b/app/views/layouts/application.liquid index b0d2d3b..7fcfbcf 100644 --- a/app/views/layouts/application.liquid +++ b/app/views/layouts/application.liquid @@ -7,13 +7,22 @@ {% render 'modules/common-styling/init', reset: true %} + - {{ content_for_layout }} + {% render 'layout/nav' %} + +
+ {{ content_for_layout }} +
{% liquid - theme_render_rc 'modules/common-styling/toasts' + function flash = 'modules/core/commands/session/get', key: 'sflash', clear: null + if context.location.pathname != flash.from or flash.force_clear + function _ = 'modules/core/commands/session/clear', key: 'sflash' + endif + render 'modules/common-styling/toasts', params: flash %} diff --git a/app/views/pages/admin/contacts/delete.liquid b/app/views/pages/admin/contacts/delete.liquid new file mode 100644 index 0000000..85ae995 --- /dev/null +++ b/app/views/pages/admin/contacts/delete.liquid @@ -0,0 +1,17 @@ +--- +slug: admin/contacts/:id +method: delete +--- +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + # platformos-check-disable ConvertIncludeToRender, UnreachableCode + + include 'modules/user/helpers/can_do_or_unauthorized', requester: current_profile, do: 'contacts.manage', redirect_anonymous_to_login: true, forbidden_partial: '403' + # platformos-check-enable ConvertIncludeToRender, UnreachableCode + + function _ = 'commands/contacts/delete', id: context.params.id + + render 'modules/core/helpers/flash/publish', notice: 'Contact has been successfully deleted', error: null, info: null + include 'modules/core/helpers/redirect_to', url: '/admin' +%} + diff --git a/app/views/pages/admin/index.liquid b/app/views/pages/admin/index.liquid new file mode 100644 index 0000000..bd224f8 --- /dev/null +++ b/app/views/pages/admin/index.liquid @@ -0,0 +1,12 @@ +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + # platformos-check-disable ConvertIncludeToRender, UnreachableCode + + include 'modules/user/helpers/can_do_or_unauthorized', requester: current_profile, do: 'admin_pages.view', redirect_anonymous_to_login: true, forbidden_partial: '403' + # platformos-check-enable ConvertIncludeToRender, UnreachableCode + + function contacts = 'queries/contacts/search', limit: 10, params: context.params + + render 'admin/index', contacts: contacts +%} + diff --git a/app/views/pages/index.html.liquid b/app/views/pages/index.html.liquid index fa512c0..7d16bfe 100644 --- a/app/views/pages/index.html.liquid +++ b/app/views/pages/index.html.liquid @@ -1,7 +1,5 @@ -{% parse_json contact %} - { - "email": "email@example.com", - "body": "Write your message here" - } - {% endparse_json %} - {% render 'contacts/form', contact: contact %} \ No newline at end of file +{% liquid + function profile = 'modules/user/helpers/current_profile' + + render 'contacts/form', contact: null +%} diff --git a/app/views/partials/403.liquid b/app/views/partials/403.liquid new file mode 100644 index 0000000..8c2c1e4 --- /dev/null +++ b/app/views/partials/403.liquid @@ -0,0 +1 @@ +

You are not authorized to see this page.

diff --git a/app/views/partials/admin/index.liquid b/app/views/partials/admin/index.liquid new file mode 100644 index 0000000..1164c42 --- /dev/null +++ b/app/views/partials/admin/index.liquid @@ -0,0 +1,55 @@ +
+
+
+ + +
+
+
+

Contacts

+
+
+
ID
+
Email
+
Body
+
Created At
+
+
+
+ {% for contact in contacts.results %} + + {% endfor %} +
+
+{% render 'modules/common-styling/pagination', total_pages: contacts.total_pages %} diff --git a/app/views/partials/layout/nav.liquid b/app/views/partials/layout/nav.liquid new file mode 100644 index 0000000..7ae94ba --- /dev/null +++ b/app/views/partials/layout/nav.liquid @@ -0,0 +1,29 @@ +{% liquid + if context.current_user + assign current_profile = context.exports.current_profile + unless current_profile + function current_profile = 'modules/user/helpers/current_profile' + endunless + endif +%} + diff --git a/modules/common-styling/public/assets/js/pos-dialog.js b/modules/common-styling/public/assets/js/pos-dialog.js new file mode 100644 index 0000000..e771e5a --- /dev/null +++ b/modules/common-styling/public/assets/js/pos-dialog.js @@ -0,0 +1,87 @@ + /* + handles openind and closing the dialog box + + usage: +*/ + + + +// purpose: opens and closes the dialog box +// arguments: +// ************************************************************************ +window.pos.modules.dialog = function(userSettings){ + + // cache 'this' value not to be overwritten later + const module = this; + + // purpose: settings that are being used across the module + // ------------------------------------------------------------------------ + module.settings = {}; + // dialog container (dom node) + module.settings.container = userSettings.container; + // modal trigger (dom node) + module.settings.trigger = userSettings.trigger; + // id used to mark the module (string) + module.settings.id = userSettings.id || module.settings.trigger.dataset.dialogtarget; + // close button (dom nodes) + module.settings.closeButtons = userSettings.closeButtons || module.settings.container?.querySelectorAll('.pos-dialog-close'); + // to enable debug mode (bool) + module.settings.debug = (userSettings?.debug) ? userSettings.debug : false; + + + + // purpose: initializes the component + // ------------------------------------------------------------------------ + module.init = () => { + pos.modules.debug(module.settings.debug, module.settings.id, 'Initializing dialog', module.settings); + + if(!module.settings.container){ + console.error('Could not find dialog container with ID ' + module.settings.id + ' while it\'s trigger is present', module.settings.trigger); + } + + module.settings.trigger.addEventListener('click', event => { + event.preventDefault(); + + module.open(); + }); + + module.settings.container?.addEventListener('toggle', event => { + if(event.newState === 'open'){ + pos.modules.debug(module.settings.debug, module.settings.id, 'Dialog opened', module.settings.container); + document.dispatchEvent(new CustomEvent('pos-dialog-opened', { bubbles: true, detail: { target: module.settings.container, id: module.settings.id } })); + pos.modules.debug(module.settings.debug, 'event', 'pos-dialog-opened', { target: module.settings.container, id: module.settings.id }); + } else if(event.newState === 'closed') { + pos.modules.debug(module.settings.debug, module.settings.id, 'Dialog closed', module.settings.container); + document.dispatchEvent(new CustomEvent('pos-dialog-closed', { bubbles: true, detail: { target: module.settings.container, id: module.settings.id } })); + pos.modules.debug(module.settings.debug, 'event', 'pos-dialog-closed', { target: module.settings.container, id: module.settings.id }); + } + }); + }; + + + // purpose: opens the dialog + // ------------------------------------------------------------------------ + module.open = () => { + module.settings.container.showModal(); + + module.settings.closeButtons.forEach(button => { + button.addEventListener('click', module.close); + }); + }; + + + // purpose: closes the dialog + // ------------------------------------------------------------------------ + module.close = () => { + module.settings.closeButtons.forEach(button => { + button.addEventListener('click', module.close); + }) + + module.settings.container.close(); + } + + + + module.init(); + +}; \ No newline at end of file diff --git a/modules/common-styling/public/assets/js/pos-forms-multiselect.js b/modules/common-styling/public/assets/js/pos-forms-multiselect.js index 9d3f066..16084e3 100644 --- a/modules/common-styling/public/assets/js/pos-forms-multiselect.js +++ b/modules/common-styling/public/assets/js/pos-forms-multiselect.js @@ -157,6 +157,11 @@ window.pos.modules.multiselect = function(container, settings){ document.addEventListener('focusin', module.reactToFocusOutside); pos.modules.debug(module.settings.debug, module.settings.id, 'Popup opened'); + + // dispatch custom event + document.dispatchEvent(new CustomEvent('pos-multiselect-opened', { bubbles: true, detail: { target: module.settings.container, id: module.settings.id, popover: module.settings.optionsNode } })); + pos.modules.debug(module.settings.debug, 'event', 'pos-multiselect-opened', { target: module.settings.container, id: module.settings.id, popover: module.settings.optionsNode }); + }; @@ -179,6 +184,11 @@ window.pos.modules.multiselect = function(container, settings){ } pos.modules.debug(module.settings.debug, module.settings.id, 'Popup closed'); + + // dispatch custom event + document.dispatchEvent(new CustomEvent('pos-multiselect-closed', { bubbles: true, detail: { container: module.settings.container, id: module.settings.id, target: module.settings.optionsNode } })); + pos.modules.debug(module.settings.debug, 'event', 'pos-multiselect-closed', { container: module.settings.container, id: module.settings.id, target: module.settings.optionsNode }); + }; @@ -249,6 +259,11 @@ window.pos.modules.multiselect = function(container, settings){ module.settings.selectedListNode.append(item); pos.modules.debug(module.settings.debug, module.settings.id, `Showed in the input: ${module.settings.availableOptions[value].label}`); + + // dispatch custom event + document.dispatchEvent(new CustomEvent('pos-multiselect-changed', { bubbles: true, detail: { container: module.settings.container, id: module.settings.id, target: module.settings.availableOptions[value], selected: module.settings.selected } })); + pos.modules.debug(module.settings.debug, 'event', 'pos-multiselect-changed', { target: module.settings.container, id: module.settings.id, target: module.settings.availableOptions[value], selected: module.settings.selected }); + }; @@ -263,6 +278,10 @@ window.pos.modules.multiselect = function(container, settings){ module.settings.selectedListNode.querySelector(`.pos-form-multiselect-selected-item-remove[for="pos-multiselect-${module.settings.id}-${value}"]`).closest('.pos-form-multiselect-selected-item').remove(); pos.modules.debug(module.settings.debug, module.settings.id, `Removed from the input: ${module.settings.availableOptions[value].label}`); + // dispatch custom event + document.dispatchEvent(new CustomEvent('pos-multiselect-changed', { bubbles: true, detail: { container: module.settings.container, id: module.settings.id, target: module.settings.availableOptions[value], selected: module.settings.selected } })); + pos.modules.debug(module.settings.debug, 'event', 'pos-multiselect-changed', { target: module.settings.container, id: module.settings.id, target: module.settings.availableOptions[value], selected: module.settings.selected }); + module.updateCounter(); }; @@ -291,6 +310,11 @@ window.pos.modules.multiselect = function(container, settings){ module.updateCounter(); pos.modules.debug(module.settings.debug, module.settings.id, `Cleared all of the selected items`); + + // dispatch custom event + document.dispatchEvent(new CustomEvent('pos-multiselect-changed', { bubbles: true, detail: { container: module.settings.container, id: module.settings.id, selected: module.settings.selected } })); + pos.modules.debug(module.settings.debug, 'event', 'pos-multiselect-changed', { target: module.settings.container, id: module.settings.id, selected: module.settings.selected }); + }; @@ -320,6 +344,11 @@ window.pos.modules.multiselect = function(container, settings){ } pos.modules.debug(module.settings.debug, module.settings.id, `Filtered options by phrase: ${phrase}`); + + // dispatch custom event + document.dispatchEvent(new CustomEvent('pos-multiselect-filtered', { bubbles: true, detail: { container: module.settings.container, id: module.settings.id, phrase: phrase } })); + pos.modules.debug(module.settings.debug, 'event', 'pos-multiselect-filtered', { target: module.settings.container, id: module.settings.id, phrase: phrase }); + }; diff --git a/modules/common-styling/public/assets/js/pos-load.js b/modules/common-styling/public/assets/js/pos-load.js index 3aeb314..e94e4dd 100644 --- a/modules/common-styling/public/assets/js/pos-load.js +++ b/modules/common-styling/public/assets/js/pos-load.js @@ -39,6 +39,7 @@ export function load(userSettings = {}){ // ------------------------------------------------------------------------ module.init = async function(){ module.settings.trigger.addEventListener(module.settings.triggerType, module.load); + module.settings.trigger.addEventListener('focus', module.load); }; @@ -70,6 +71,7 @@ export function load(userSettings = {}){ // ------------------------------------------------------------------------ module.destroy = function(){ module.settings.trigger.removeEventListener('click', module.load); + module.settings.trigger.removeEventListener('focus', module.load); pos.modules.debug(module.settings.debug, module.settings.id, 'Destroyed module', module.settings); module.settings = {}; }; diff --git a/modules/common-styling/public/assets/js/pos-popover.js b/modules/common-styling/public/assets/js/pos-popover.js index 89225aa..ae1ad4a 100644 --- a/modules/common-styling/public/assets/js/pos-popover.js +++ b/modules/common-styling/public/assets/js/pos-popover.js @@ -1,4 +1,4 @@ -/* + /* handles focus for popover menus and provides fallback for firefox lacking anchor positioning usage: @@ -17,7 +17,7 @@ window.pos.modules.popover = function(container, userSettings = {}){ // purpose: settings that are being used across the module // ------------------------------------------------------------------------ module.settings = {}; - // notifications container (dom node) + // popover container (dom node) module.settings.container = container || document.querySelector('.pos-popover'); // popover trigger (dom node) module.settings.trigger = module.settings.container.querySelector('[popovertarget]'); @@ -29,21 +29,30 @@ window.pos.modules.popover = function(container, userSettings = {}){ module.settings.opened = false; // menu element inside the popover (dom node) module.settings.menu = module.settings.popover.matches('menu') ? module.settings.popover : module.settings.popover.querySelector('menu'); + // all of the focusable elements in the menu (array) + module.settings.focusable = []; // to enable debug mode (bool) module.settings.debug = (userSettings?.debug) ? userSettings.debug : false; - + // purpose: initializes the component // ------------------------------------------------------------------------ module.init = () => { pos.modules.debug(module.settings.debug, module.settings.id, 'Initializing popover menu', module.settings.container); - module.settings.popover.addEventListener('beforetoggle', event => { + module.settings.popover.addEventListener('toggle', event => { if(event.newState == 'open'){ + // set all the focusable menu items + if(module.settings.menu){ + module.settings.focusable = Array.from(module.settings.menu.querySelectorAll('li a, li button')).filter(element => element.checkVisibility()) + } + + // set state module.settings.opened = true; pos.modules.debug(module.settings.debug, module.settings.id, 'Popover opened', module.settings.container); + // dispatch custom event document.dispatchEvent(new CustomEvent('pos-popover-opened', { bubbles: true, detail: { target: module.settings.popover, id: module.settings.id } })); pos.modules.debug(module.settings.debug, 'event', 'pos-popover-opened', { target: module.settings.popover, id: module.settings.id }); @@ -52,9 +61,11 @@ window.pos.modules.popover = function(container, userSettings = {}){ document.addEventListener('keydown', module.keyboard); } } else { + // set state module.settings.opened = false; pos.modules.debug(module.settings.debug, module.settings.id, 'Popover closed', module.settings.container); + // dispatch custom event document.dispatchEvent(new CustomEvent('pos-popover-closed', { bubbles: true, detail: { target: module.settings.popover, id: module.settings.id } })); pos.modules.debug(module.settings.debug, 'event', 'pos-popover-closed', { target: module.settings.popover, id: module.settings.id }); @@ -134,7 +145,7 @@ window.pos.modules.popover = function(container, userSettings = {}){ event.preventDefault(); if(module.settings.menu.contains(document.activeElement)){ - if(document.activeElement.closest('li').nextElementSibling){ + if(module.settings.focusable[module.settings.focusable.indexOf(document.activeElement)+1]){ module.focusNextMenuItem(); } else { pos.modules.debug(module.settings.debug, module.settings.id, 'There is no next menu item', module.settings.container); @@ -149,7 +160,7 @@ window.pos.modules.popover = function(container, userSettings = {}){ event.preventDefault(); if(module.settings.menu.contains(document.activeElement)){ - if(document.activeElement.closest('li').previousElementSibling){ + if(module.settings.focusable[module.settings.focusable.indexOf(document.activeElement)-1]){ module.focusPreviousMenuItem(); } else { pos.modules.debug(module.settings.debug, module.settings.id, 'There is no previous menu item', module.settings.container); @@ -172,32 +183,35 @@ window.pos.modules.popover = function(container, userSettings = {}){ }; - // purpose: focuses first menu item + // purpose: focuses first visible menu item // ------------------------------------------------------------------------ module.focusFirstMenuItem = () => { - pos.modules.debug(module.settings.debug, module.settings.id, 'Focusing first menu item', module.settings.container); - module.settings.menu.querySelector('li:first-child a, li:first-child button').focus(); + module.settings.focusable[0].focus(); + pos.modules.debug(module.settings.debug, module.settings.id, 'Focusing first visible menu item', module.settings.focusable[0]); }; - // purpose: focuses last menu item + // purpose: focuses last visible menu item // ------------------------------------------------------------------------ module.focusLastMenuItem = () => { - pos.modules.debug(module.settings.debug, module.settings.id, 'Focusing last menu item', module.settings.container); - module.settings.menu.querySelector('li:last-child a, li:last-child button').focus(); + module.settings.focusable[module.settings.focusable.length-1].focus(); + pos.modules.debug(module.settings.debug, module.settings.id, 'Focusing last visible menu item', module.settings.focusable[module.settings.focusable.length-1]); }; // purpose: focuses menu item that is next to the currently focused one // ------------------------------------------------------------------------ module.focusNextMenuItem = () => { - pos.modules.debug(module.settings.debug, module.settings.id, 'Focusing next available menu item', module.settings.container); - document.activeElement.closest('li').nextElementSibling.querySelector('a, button').focus(); + const currentlyFocused = module.settings.focusable.indexOf(document.activeElement); + module.settings.focusable[currentlyFocused+1].focus(); + pos.modules.debug(module.settings.debug, module.settings.id, 'Focusing next available menu item', module.settings.focusable[currentlyFocused+1]); }; // purpose: focuses menu item that is previous to the currently focused one // ------------------------------------------------------------------------ module.focusPreviousMenuItem = () => { - pos.modules.debug(module.settings.debug, module.settings.id, 'Focusing previous available menu item', module.settings.container); - document.activeElement.closest('li').previousElementSibling.querySelector('a, button').focus(); + const currentlyFocused = module.settings.focusable.indexOf(document.activeElement); + module.settings.focusable[currentlyFocused-1].focus(); + + pos.modules.debug(module.settings.debug, module.settings.id, 'Focusing previous available menu item', module.settings.focusable[currentlyFocused-1]); }; @@ -207,20 +221,27 @@ window.pos.modules.popover = function(container, userSettings = {}){ pos.modules.debug(module.settings.debug, module.settings.id, 'This browser does not support anchor positioning, setting the position manually', module.settings.container); const triggerSize = module.settings.trigger.getBoundingClientRect(); + triggerSize.offsetBottom = triggerSize.bottom + window.scrollY; const popoverSize = module.settings.popover.getBoundingClientRect(); module.settings.popover.style.position = 'absolute'; + // position to top + module.settings.popover.style.top = triggerSize.offsetBottom + 'px'; + // position to right if(triggerSize.left - popoverSize.width > 0){ + module.settings.popover.style.left = 'auto'; module.settings.popover.style.right = window.innerWidth - triggerSize.right + 'px'; } // position to left else if(triggerSize.left + popoverSize.width < window.innerWidth){ + module.settings.popover.style.right = 'auto'; module.settings.popover.style.left = triggerSize.left + 'px'; } // position to center else { + module.settings.popover.style.right = 'auto'; module.settings.popover.style.left = triggerSize.left + (triggerSize.width - popoverSize.width) / 2 + 'px'; } }; diff --git a/modules/common-styling/public/assets/style-guide/styleguide.css b/modules/common-styling/public/assets/style-guide/styleguide.css index 8a2ec02..ef08fd7 100644 --- a/modules/common-styling/public/assets/style-guide/styleguide.css +++ b/modules/common-styling/public/assets/style-guide/styleguide.css @@ -9,6 +9,10 @@ gap: calc(var(--pos-padding-page) * 5); } +article { + scroll-margin: var(--pos-padding-page); +} + article + article { margin-block-start: var(--pos-gap-section-section); } @@ -119,8 +123,10 @@ p code { } .styleguide-copy .styleguide-button-label { + width: 100%; + height: 100%; position: absolute; - inset-inline-start: -100vw; + inset-inline-start: -300vw; } .styleguide-copy svg { @@ -364,6 +370,7 @@ p code { } #icons li { + width: 94px; display: flex; flex-direction: column; gap: .3em; @@ -374,17 +381,26 @@ p code { transition: color .1s linear; } +#icons li > span { + width: 94px; + overflow: hidden; + + white-space: nowrap; + text-align: center; + text-overflow: ellipsis; +} + #icons ul svg { - width: 64px; - height: 64px; - padding: 20px; + width: 94px; + height: 94px; + padding: 30px; background-color: var(--pos-color-content-background); border-radius: var(--pos-radius-panel); color: var(--pos-color-content-text); - transition: color .1s linear; + transition: color .1s linear, scale .2s var(--pos-transition-ui-detail); } #icons li:not(.styleguide-icon-copied):hover, @@ -397,16 +413,24 @@ p code { color: var(--pos-color-confirmation); } + #icons .styleguide-icon-copied svg { + scale: .9; + } + /* fonts ============================================================================ */ -#fonts p { +#fonts p + p { margin-block-start: var(--pos-gap-text-text); line-height: 1.3em; } +.styleguide-fonts-prose { + min-height: 7lh; +} + .styleguide-fonts-example { margin-block: 2rem .5rem; display: flex; @@ -609,6 +633,14 @@ p code { margin-inline-start: auto; } +.styleguide-forms-placeholder-tag { + width: 80px; + height: 1.5rem; + + background-color: var(--pos-color-highlight-background); + border-radius: var(--pos-radius-tag); +} + #forms .styleguide-details + p { margin-block-start: calc(var(--pos-gap-text-text) / 2); } @@ -638,4 +670,49 @@ p code { margin: 0; border-inline-start: 3px dotted var(--pos-color-content-background); - } \ No newline at end of file + } + + +/* spacings +============================================================================ */ +.styleguide-spacings-example { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; +} + +.styleguide-spacings-example:before { + width: 24px; + height: 24px; + position: absolute; + inset-inline-end: 2.2rem; + background: var(--pos-color-content-text); + + clip-path: path('M12.25 2.94H14l-2-3-2 3h1.75V21H10l2 3 2-3h-1.75V2.94z'); + + content: ''; +} + +.styleguide-spacings-example-value { + padding: 2px 3px; + position: absolute; + inset-inline-end: .5rem; + + background-color: var(--pos-color-content-text); + border-radius: var(--pos-radius-tag); + + text-transform: uppercase; + font-size: .75rem; + color: var(--pos-color-content-background); +} + + +/* placeholders +============================================================================ */ +.styleguide-placeholder-input { + height: var(--pos-height-input); + + border-radius: var(--pos-radius-input); + background-color: var(--pos-color-highlight-background); +} \ No newline at end of file diff --git a/modules/common-styling/public/assets/style-guide/styleguide.js b/modules/common-styling/public/assets/style-guide/styleguide.js index 70ba2ba..ba16637 100644 --- a/modules/common-styling/public/assets/style-guide/styleguide.js +++ b/modules/common-styling/public/assets/style-guide/styleguide.js @@ -52,6 +52,7 @@ posStyleGuide.colors = () => { module.wrapIcons(); module.showIconNames(); module.copyIcon(); + module.addMeasurements(); }; @@ -128,9 +129,9 @@ posStyleGuide.colors = () => { document.querySelectorAll('.styleguide-code').forEach(element => { element.appendChild(copyButton.content.cloneNode(true)); - element.addEventListener('click', event => { + element.querySelector('.styleguide-copy').addEventListener('click', event => { // text to copy to the clipboard (string) - const text = element.parentElement.querySelector('pre code').textContent.trim(); + const text = element.querySelector('pre code').textContent.trim(); // copy code to clipboard navigator.clipboard.writeText(text).then(() => { @@ -153,8 +154,6 @@ posStyleGuide.colors = () => { module.settings.iconNodes.forEach(item => { let wrapper = document.createElement('li'); - wrapper.classList.add('flex', 'flex-col', 'items-center', 'cursor-pointer'); - item.parentNode.insertBefore(wrapper, item); wrapper.appendChild(item); }); @@ -167,7 +166,8 @@ posStyleGuide.colors = () => { module.showIconNames = () => { module.settings.iconNodes.forEach(item => { - item.insertAdjacentHTML('afterend', item.getAttribute('data-icon')); + item.insertAdjacentHTML('afterend', `${item.getAttribute('data-icon')}`); + item.parentElement.title = `Copy '${item.getAttribute('data-icon')}' icon code to clipboard`; }); }; @@ -194,6 +194,15 @@ posStyleGuide.colors = () => { }; + // purpose: adds measurements to the spacings section + // ------------------------------------------------------------------------ + module.addMeasurements = () => { + document.querySelectorAll('#spacings .styleguide-spacings-example').forEach(element => { + element.innerHTML += `
${(window.getComputedStyle(element.parentElement.querySelector('.styleguide-placeholder-input:last-child'))).getPropertyValue('margin-block-start')}
`; + }); + }; + + module.init(); }; diff --git a/modules/common-styling/public/assets/style/pos-avatar.css b/modules/common-styling/public/assets/style/pos-avatar.css index 28273d9..60a7919 100644 --- a/modules/common-styling/public/assets/style/pos-avatar.css +++ b/modules/common-styling/public/assets/style/pos-avatar.css @@ -36,14 +36,14 @@ font-size: .6rem; } -.pos-avatar-s { +.pos-avatar-sm { width: 24px; height: 24px; font-size: .7rem; } -.pos-avatar-m { +.pos-avatar-md { width: 32px; height: 32px; @@ -51,7 +51,7 @@ } -.pos-avatar-l { +.pos-avatar-lg { width: 48px; height: 48px; @@ -65,14 +65,14 @@ font-size: 2rem; } -.pos-avatar-xxl { +.pos-avatar-2xl { width: 160px; height: 160px; font-size: 3rem; } -.pos-avatar-xxxl { +.pos-avatar-3xl { width: 192px; height: 192px; diff --git a/modules/common-styling/public/assets/style/pos-card.css b/modules/common-styling/public/assets/style/pos-card.css index 2f2ea81..2afb357 100644 --- a/modules/common-styling/public/assets/style/pos-card.css +++ b/modules/common-styling/public/assets/style/pos-card.css @@ -4,6 +4,7 @@ basic highlighted content + alert */ @@ -110,4 +111,64 @@ width: .75rem; } + + /* alert + ============================================================================ */ + .pos-card-alert { + display: flex; + align-items: center; + gap: .5em; + + background-color: var(--pos-color-highlight-background); + border: 1px solid var(--pos-color-frame); + + color: var(--pos-color-highlight-text); + } + + .pos-card-alert + .pos-card, + .pos-card-alert + .pos-community-space-header { + margin-block-start: var(--pos-gap-card-card); + } + + .pos-card + .pos-card-alert, + .pos-community-space-header + .pos-card-alert { + margin-block-start: var(--pos-gap-card-card); + } + + .pos-card-alert svg { + width: 1.5rem; + + fill: var(--pos-color-content-text-supplementary); + } + + /* warning */ + .pos-card-alert-warning { + background-color: var(--pos-color-warning-disabled); + border-color: var(--pos-color-warning); + } + + .pos-card-alert-warning svg { + fill: var(--pos-color-warning); + } + + /* error */ + .pos-card-alert-error { + background-color: var(--pos-color-important-disabled); + border-color: var(--pos-color-important); + } + + .pos-card-alert-error svg { + fill: var(--pos-color-important); + } + + /* success */ + .pos-card-alert-success { + background-color: var(--pos-color-confirmation-disabled); + border-color: var(--pos-color-confirmation); + } + + .pos-card-alert-success svg { + fill: var(--pos-color-confirmation); + } + } \ No newline at end of file diff --git a/modules/common-styling/public/assets/style/pos-config.css b/modules/common-styling/public/assets/style/pos-config.css index 11ad6bf..cf6b0d4 100644 --- a/modules/common-styling/public/assets/style/pos-config.css +++ b/modules/common-styling/public/assets/style/pos-config.css @@ -23,6 +23,7 @@ spacing button shape input shape + transitions */ @@ -768,7 +769,7 @@ /* gap between section title and content */ --pos-gap-title-content: 32px; /* gap between sections */ - --pos-gap-section-section: 80px; + --pos-gap-section-section: 40px; /* gap between cards */ --pos-gap-card-card: 16px; /* gap between text content */ @@ -835,4 +836,12 @@ --pos-height-button: var(--pos-height-input); /* border width for inputs */ --pos-border-button: var(--pos-border-input); +} + + + +/* transitions */ +/* ================================================================================ */ +:root { + --pos-transition-ui-detail: cubic-bezier(.3, 1, .06, 1); } \ No newline at end of file diff --git a/modules/common-styling/public/assets/style/pos-dialog.css b/modules/common-styling/public/assets/style/pos-dialog.css new file mode 100644 index 0000000..589d143 --- /dev/null +++ b/modules/common-styling/public/assets/style/pos-dialog.css @@ -0,0 +1,149 @@ +/* + dialog window + + layout + looks + content + backdrop +*/ + + + +@layer common-styling { + + /* layout + ============================================================================ */ + .pos-dialog { + width: fit-content; + min-width: 400px; + height: fit-content; + margin: auto; + position: fixed; + inset: 0; + overflow: auto; + } + + @media (max-width: 500px) { + .pos-dialog { + width: calc(100vw - 2 * var(--pos-padding-page)); + min-width: 0; + } + } + + + /* looks + ============================================================================ */ + .pos-dialog[open] { + opacity: 1; + scale: 1; + } + + .pos-dialog { + max-width: 600px; + max-height: calc(100vh - 4 * var(--pos-padding-page)); + padding: var(--pos-padding-card); + + opacity: 0; + border-radius: var(--pos-radius-card); + background-color: var(--pos-color-content-background); + + scale: 0; + + transition: + opacity .3s var(--pos-transition-ui-detail), + scale .3s var(--pos-transition-ui-detail), + overlay 7s ease-out allow-discrete, + display 7s ease-out allow-discrete; + } + + @starting-style { + .pos-dialog[open] { + opacity: 0; + scale: 0; + } + } + + + /* content + ============================================================================ */ + .pos-dialog-header { + display: flex; + align-items: start; + justify-content: space-between; + margin-block-end: var(--pos-gap-section-elements); + } + + .pos-dialog-header-simple { + margin-block-end: -1rem; + justify-content: flex-end; + } + + .pos-dialog-header .pos-heading-2 { + margin-block-start: -.3em; + } + + .pos-dialog-header .pos-dialog-close { + width: 1.5rem; + height: 1.5rem; + padding-block-start: .2em; + flex-shrink: 0; + position: relative; + z-index: 1; + + cursor: pointer; + border-radius: var(--pos-radius-button); + + transition: scale .2s var(--pos-transition-ui-detail); + } + + .pos-dialog-header .pos-dialog-close:hover { + color: var(--pos-color-interactive-hover); + } + + .pos-dialog-header .pos-dialog-close:active { + scale: .9; + } + + .pos-dialog-header .pos-dialog-close:focus-visible { + outline: 2px solid var(--pos-color-focused); + outline-offset: 2px; + } + + .pos-dialog-header .pos-label { + position: absolute; + left: -200vw; + } + + .pos-dialog-actions { + margin-block-start: var(--pos-gap-section-elements); + display: flex; + flex-wrap: wrap; + gap: var(--pos-gap-button-button); + justify-content: center; + } + + + /* backdrop + ============================================================================ */ + .pos-dialog::backdrop { + background-color: transparent; + transition: + display .7s allow-discrete, + overlay .7s allow-discrete, + background-color .3s, + backdrop-filter .3s var(--pos-transition-ui-detail); + } + + .pos-dialog:open::backdrop { + background-color: color-mix(in srgb, var(--pos-color-page-background) 70%, transparent); + backdrop-filter: blur(2px); + } + + @starting-style { + .pos-dialog:open::backdrop { + background-color: transparent; + } + } + + +} \ No newline at end of file diff --git a/modules/common-styling/public/assets/style/pos-forms.css b/modules/common-styling/public/assets/style/pos-forms.css index fe1693f..bf9ea0f 100644 --- a/modules/common-styling/public/assets/style/pos-forms.css +++ b/modules/common-styling/public/assets/style/pos-forms.css @@ -26,9 +26,6 @@ ============================================================================ */ .pos-form fieldset, .pos-form-fieldset { - all: unset; - display: revert; - box-sizing: border-box; } @@ -52,7 +49,7 @@ } /* combined fieldset */ - .pos-form .pos-form-fieldset-combined { + .pos-form-fieldset-combined { display: flex; align-items: center; } @@ -79,17 +76,40 @@ /* labels ============================================================================ */ - .pos-form-simple fieldset:has(label + input) label { - margin-inline-start: -.1em; + .pos-form-simple fieldset:has(label + input) label, + .pos-form-simple fieldset:has(label + textarea) label, + .pos-form-simple fieldset:has(label + .hashtag-container) label, + .pos-form-simple fieldset:has(label + .uppy-container) label { + margin-inline-start: -.1ex; + margin-block-end: .1em; align-self: start; } - .pos-form :has([required]) label:after { + .pos-form :has([required]:not([type="radio"])) label:after, + .pos-form :has([required]:not([type="radio"])) label strong:after { color: var(--pos-color-important); content: " *"; } + .pos-form :has([required]) label:has(strong):after { + display: none; + } + + .pos-form label:has(strong) { + display: flex; + flex-direction: column; + } + + .pos-form label strong { + font-weight: 400; + } + + .pos-form label small { + font-size: .85em; + color: var(--pos-color-content-text-supplementary); + } + /* checkbox ============================================================================ */ @@ -135,6 +155,8 @@ .pos-form [type="email"], .pos-form [type="password"], .pos-form [type="url"], + .pos-form [type="datetime"], + .pos-form [type="datetime-local"], .pos-form textarea { all: unset; display: revert; @@ -158,12 +180,14 @@ transition-timing-function: linear; } - .pos-form-input:not(:disabled):hover, - .pos-form [type="text"]:not(:disabled):hover, - .pos-form [type="email"]:not(:disabled):hover, - .pos-form [type="password"]:not(:disabled):hover, - .pos-form [type="url"]:not(:disabled):hover, - .pos-form textarea:not(:disabled):hover, + .pos-form-input:not(:disabled):not(:focus-visible):hover, + .pos-form [type="text"]:not(:disabled):not(:focus-visible):hover, + .pos-form [type="email"]:not(:disabled):not(:focus-visible):hover, + .pos-form [type="password"]:not(:disabled):not(:focus-visible):hover, + .pos-form [type="url"]:not(:disabled):not(:focus-visible):hover, + .pos-form [type="datetime"]:not(:disabled):not(:focus-visible):hover, + .pos-form [type="datetime-local"]:not(:disabled):not(:focus-visible):hover, + .pos-form textarea:not(:disabled):not(:focus-visible):hover, .pos-debug-form-input-hover { border-color: var(--pos-color-input-hover-frame); background-color: var(--pos-color-input-hover-background); @@ -176,6 +200,8 @@ .pos-form [type="email"]:focus-visible, .pos-form [type="password"]:focus-visible, .pos-form [type="url"]:focus-visible, + .pos-form [type="datetime"]:focus-visible, + .pos-form [type="datetime-local"]:focus-visible, .pos-form textarea:focus-visible, .pos-debug-form-input-focus-visible { position: relative; @@ -185,14 +211,16 @@ background-color: var(--pos-color-input-active-background); outline: 2px solid var(--pos-color-focused); - color: var(--pos-color-input-focus-text); + color: var(--pos-color-input-active-text); } .pos-form-input:disabled, .pos-form [type="text"]:disabled, .pos-form [type="email"]:disabled, .pos-form [type="password"]:disabled, - .pos-form [type="url"]:disabled + .pos-form [type="url"]:disabled, + .pos-form [type="datetime"]:disabled, + .pos-form [type="datetime-local"]:disabled, .pos-form textarea:disabled { border-color: var(--pos-color-input-disabled-frame); background-color: var(--pos-color-input-disabled-background); @@ -205,6 +233,8 @@ .pos-form [type="email"]::placeholder, .pos-form [type="password"]::placeholder, .pos-form [type="url"]::placeholder, + .pos-form [type="datetime"]::placeholder, + .pos-form [type="datetime-local"]::placeholder, .pos-form textarea::placeholder { color: var(--pos-color-input-placeholder); } @@ -287,7 +317,10 @@ .pos-form [type="text"][aria-invalid="true"], .pos-form [type="email"][aria-invalid="true"], .pos-form [type="password"][aria-invalid="true"], - .pos-form [type="url"][aria-invalid="true"] { + .pos-form [type="url"][aria-invalid="true"], + .pos-form [type="datetime"][aria-invalid="true"], + .pos-form [type="datetime-local"][aria-invalid="true"], + .pos-form textarea[aria-invalid="true"] { border-color: var(--pos-color-important); } @@ -297,6 +330,16 @@ } + /* form actions, submit buttons + ============================================================================ */ + .pos-form-actions { + margin-block-start: var(--pos-gap-section-elements); + display: flex; + justify-content: end; + gap: var(--pos-gap-button-button); + } + + /* password input ============================================================================ */ .pos-form-password { diff --git a/modules/common-styling/public/assets/style/pos-pagination.css b/modules/common-styling/public/assets/style/pos-pagination.css index 5a92588..6ce932d 100644 --- a/modules/common-styling/public/assets/style/pos-pagination.css +++ b/modules/common-styling/public/assets/style/pos-pagination.css @@ -9,9 +9,25 @@ .pos-pagination { margin-block-start: var(--pos-gap-card-card); display: flex; + align-items: center; justify-content: end; } + .pos-pagination svg { + height: 100%; + } + + .pos-pagination .pos-label { + position: absolute; + left: -200vw; + } + + .pos-pagination .pos-pagination-previous, + .pos-pagination .pos-pagination-next { + height: 1.25em !important; + padding-inline: .75em; + } + .pos-pagination ul { display: flex; gap: calc(var(--pos-gap-text-text) / 2); diff --git a/modules/common-styling/public/assets/style/pos-popover.css b/modules/common-styling/public/assets/style/pos-popover.css index e32b4b9..50c110d 100644 --- a/modules/common-styling/public/assets/style/pos-popover.css +++ b/modules/common-styling/public/assets/style/pos-popover.css @@ -21,7 +21,8 @@ max-width: 600px; margin-block-start: 1rem; position-area: block-end span-inline-start; - position-try-fallbacks: flip-inline; + position-try-options: flip-block, flip-inline; + position-try-fallbacks: flip-block, flip-inline; overflow: hidden; border-radius: var(--pos-radius-panel); @@ -32,9 +33,11 @@ .pos-popover [popover] { width: calc(100vw - var(--pos-padding-page) * 2) !important; position-area: none; + position-try-options: none; + position-try-fallbacks: none; position: absolute; inset-block-start: anchor(bottom); - inset-inline-start: var(--pos-padding-page); + inset-inline-start: var(--pos-padding-page) !important; } } @@ -66,7 +69,7 @@ width: 100%; padding: 1rem .75rem; display: flex; - gap: .5em; + gap: .6em; align-items: center; cursor: pointer; @@ -94,7 +97,27 @@ width: 1.2em; height: 1.2em; - color: inherit; + color: var(--pos-color-content-text-supplementary); } + .pos-popover menu li a:hover svg, + .pos-popover menu li a:focus-visible svg, + .pos-popover menu li button:hover svg, + .pos-popover menu li button:focus-visible svg { + color: inherit; + } + + .pos-popover .pos-popover-menu-item-important button, + .pos-popover .pos-popover-menu-item-important a, + .pos-popover .pos-popover-menu-item-important svg { + color: var(--pos-color-important); + } + + .pos-popover .pos-popover-menu-item-important a:hover, + .pos-popover .pos-popover-menu-item-important button:hover, + .pos-popover .pos-popover-menu-item-important a:focus-visible, + .pos-popover .pos-popover-menu-item-important button:focus-visible { + background-color: var(--pos-color-important); + } + } \ No newline at end of file diff --git a/modules/common-styling/public/assets/style/pos-table.css b/modules/common-styling/public/assets/style/pos-table.css index 7c685e8..8b1fac3 100644 --- a/modules/common-styling/public/assets/style/pos-table.css +++ b/modules/common-styling/public/assets/style/pos-table.css @@ -17,17 +17,17 @@ } /* header */ - .pos-table header { + .pos-table > header { text-transform: uppercase; color: var(--pos-color-content-text-supplementary); } @media (min-width: 801px) { - .pos-table header { + .pos-table > header { display: table-row; } - .pos-table header > * { + .pos-table > header > * { padding-inline: var(--pos-padding-cell); padding-block: calc(var(--pos-padding-cell) / 2); display: table-cell; @@ -35,17 +35,17 @@ font-size: .9rem; } - .pos-table header > :first-child { + .pos-table > header > :first-of-type { padding-inline-start: 0; } - .pos-table header > :last-child { + .pos-table > header > :last-of-type { padding-inline-end: 0; } } @media (max-width: 800px) { - .pos-table header { + .pos-table > header { display: none; } } @@ -65,42 +65,72 @@ display: table-cell; } - .pos-table-content > ul > li:first-child { + .pos-table-content > ul > li:first-of-type { padding-inline-start: 0; } - .pos-table-content > ul > li:last-child { + .pos-table-content > ul > li:last-of-type { padding-inline-end: 0; } - .pos-table-content > ul:not(:first-child) li { + .pos-table-content > ul:not(:first-of-type) > li { border-block-start: 1px solid var(--pos-color-frame); } - .pos-table-content:not(.pos-card) > ul:last-child li { + .pos-table-content:not(.pos-card) > ul:last-of-type > li { padding-block-end: 0; } } + @media (max-width: 800px) { + .pos-table-content > ul { + display: flex; + flex-direction: column; + gap: calc(var(--pos-padding-cell) / 2); + } + } + /* content, card variant */ @media (min-width: 801px) { - .pos-table-content.pos-card > ul:first-child > li:first-child { + .pos-table-content.pos-card > ul:first-of-type > li:first-of-type { border-top-left-radius: var(--pos-radius-card); } - .pos-table-content.pos-card > ul:first-child > li:last-child { - border-top-left-radius: var(--pos-radius-card); + .pos-table-content.pos-card > ul:first-of-type > li:last-of-type { + border-top-right-radius: var(--pos-radius-card); } - .pos-table-content.pos-card > ul > li:first-child { + .pos-table-content.pos-card > ul:last-of-type > li:first-of-type { + border-bottom-left-radius: var(--pos-radius-card); + } + + .pos-table-content.pos-card > ul:last-of-type > li:last-of-type { + border-bottom-right-radius: var(--pos-radius-card); + } + + .pos-table-content.pos-card > ul > li:first-of-type { padding-inline-start: var(--pos-padding-cell); } - .pos-table-content.pos-card > ul > li:last-child { + .pos-table-content.pos-card > ul > li:last-of-type { padding-inline-end: var(--pos-padding-cell); } } + @media (max-width: 800px) { + .pos-table-content.pos-card { + padding: 0; + } + + .pos-table-content.pos-card > ul { + padding: var(--pos-padding-card); + } + + .pos-table-content.pos-card > ul:not(:first-of-type) { + border-block-start: 1px solid var(--pos-color-page-background); + } + } + /* text formatting */ @media (min-width: 801px) { .pos-table-number { @@ -110,6 +140,14 @@ .pos-table-content-heading { display: none; } + + .pos-table-cell-priority { + width: 100%; + } + + .pos-table-cell-nowrap { + white-space: nowrap; + } } @media (max-width: 800px) { diff --git a/modules/common-styling/public/assets/style/pos-tag.css b/modules/common-styling/public/assets/style/pos-tag.css index 80f80d3..27435f9 100644 --- a/modules/common-styling/public/assets/style/pos-tag.css +++ b/modules/common-styling/public/assets/style/pos-tag.css @@ -3,7 +3,10 @@ tag list basic - clickable + confirmation + warning + important + interactive */ @@ -20,20 +23,64 @@ /* basic ============================================================================ */ .pos-tag { + min-height: 1.5rem; padding: .33em 1rem; + display: inline-flex; + align-items: center; + gap: .5em; border-radius: var(--pos-radius-tag); background-color: var(--pos-color-page-background); text-transform: uppercase; + line-height: 0; font-size: .75rem; } - - - /* clickable + + .pos-tag svg { + width: 1rem; + height: 1rem; + } + + + /* confirmation + ============================================================================ */ + .pos-tag.pos-tag-confirmation { + background-color: var(--pos-color-confirmation-disabled); + } + + + /* warning + ============================================================================ */ + .pos-tag.pos-tag-warning { + background-color: var(--pos-color-warning-disabled); + } + + + /* error + ============================================================================ */ + .pos-tag.pos-tag-important { + background-color: var(--pos-color-important-disabled); + } + + /* interactive ============================================================================ */ - .pos-card.pos-card-highlighted { - background-color: var(--pos-color-highlight-background); + .pos-tag.pos-tag-interactive { + background-color: var(--pos-color-interactive); + cursor: pointer; + + color: var(--pos-color-content-inverted-text); + } + + a.pos-tag-interactive:hover, + button.pos-tag-interactive:hover { + background-color: var(--pos-color-interactive-hover); + } + + a.pos-tag-interactive:focus-visible, + button.pos-tag-interactive:focus-visible { + outline: 2px solid var(--pos-color-focused); + outline-offset: 2px; } } \ No newline at end of file diff --git a/modules/common-styling/public/assets/style/pos-toast.css b/modules/common-styling/public/assets/style/pos-toast.css index 7f17ca6..039f629 100644 --- a/modules/common-styling/public/assets/style/pos-toast.css +++ b/modules/common-styling/public/assets/style/pos-toast.css @@ -16,6 +16,7 @@ .pos-toasts { width: 300px; position: fixed; + z-index: 10; inset-block-end: var(--pos-padding-page); inset-inline-start: var(--pos-padding-page); display: flex; diff --git a/modules/common-styling/public/assets/style/pos-typography.css b/modules/common-styling/public/assets/style/pos-typography.css index 68ec298..f239c82 100644 --- a/modules/common-styling/public/assets/style/pos-typography.css +++ b/modules/common-styling/public/assets/style/pos-typography.css @@ -5,6 +5,7 @@ links headings sidenotes + utility classes increasing text legibility long text */ @@ -77,6 +78,7 @@ margin-inline-start: -.1ex; display: block; + line-height: 1.2; font-family: var(--pos-font-heading); font-size: 2rem; font-weight: 500; @@ -99,6 +101,14 @@ font-weight: 400; } + .pos-heading-3 + .pos-card { + margin-block-start: .3em; + } + + .pos-card + .pos-heading-3 { + margin-block-start: var(--pos-gap-card-card); + } + /* sidenotes ============================================================================ */ @@ -110,7 +120,7 @@ /* tips ============================================================================ */ .pos-tip { - margin-block-start: .5rem; + margin-block-start: .2rem; position: relative; display: flex; gap: .5em; @@ -122,11 +132,18 @@ width: 1.2rem; height: 1.2rem; position: relative; - top: 3px; + top: 2px; flex-shrink: 0; } + /* utility classes + ============================================================================ */ + .pos-text-center { + text-align: center; + } + + /* increasing text legibility ============================================================================ */ .pos-increaseLegibility { @@ -155,10 +172,31 @@ /* long text ============================================================================ */ + .pos-prose .pos-heading-2 { + margin-block-end: .1em; + } + .pos-prose .pos-heading-3 { margin-block-end: .5rem; } + .pos-prose * + .pos-heading-3 { + margin-block-start: 1em; + } + + .pos-prose p { + line-height: 1.4; + } + + .pos-prose p + p { + margin-block-start: .5em; + } + + .pos-prose * + ol, + .pos-prose * + ul { + margin-block-start: .5em; + } + .pos-prose ol { margin-inline-start: 1.2em; diff --git a/modules/common-styling/public/assets/style/pos-utility.css b/modules/common-styling/public/assets/style/pos-utility.css new file mode 100644 index 0000000..be38429 --- /dev/null +++ b/modules/common-styling/public/assets/style/pos-utility.css @@ -0,0 +1,50 @@ +/* + utility classes for spacings and layout + + headings +*/ + + +@layer common-styling { + + /* headings + ============================================================================ */ + .pos-heading-with-action { + margin-block-end: var(--pos-gap-title-content); + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: var(--pos-gap-button-button); + } + + .pos-heading-with-action small { + margin-inline-start: .2ch; + display: block; + + line-height: 1.5em; + font-size: .8rem; + color: var(--pos-color-content-text-supplementary) + } + + + + /* gaps + ============================================================================ */ + .pos-gap-section-section { + gap: var(--pos-gap-section-section); + } + + .pos-mt-section-section { + margin-block-start: var(--pos-gap-section-section); + } + + .pos-gap-text-text { + gap: var(--pos-gap-text-text); + } + + .pos-mt-text-text { + margin-block-start: var(--pos-gap-text-text); + } + +} \ No newline at end of file diff --git a/modules/common-styling/public/views/layouts/style-guide.liquid b/modules/common-styling/public/views/layouts/style-guide.liquid index 5df7352..be3e72d 100644 --- a/modules/common-styling/public/views/layouts/style-guide.liquid +++ b/modules/common-styling/public/views/layouts/style-guide.liquid @@ -19,7 +19,7 @@ {{ content_for_layout }} {% liquid - theme_render_rc 'modules/common-styling/toasts' + render 'modules/common-styling/toasts' %} diff --git a/modules/common-styling/public/views/pages/style-guide.liquid b/modules/common-styling/public/views/pages/style-guide.liquid index 5209b48..9ce6ea2 100644 --- a/modules/common-styling/public/views/pages/style-guide.liquid +++ b/modules/common-styling/public/views/pages/style-guide.liquid @@ -3,1744 +3,53 @@ layout: 'modules/common-styling/style-guide' ---
-
- - -
-

Initlialization

-
-
-

All of the following CSS (except CSS custom properties) are scoped to container that uses pos-app class. You can use that class either on the root html tag of your app or just limit the scope to any container.

-{% capture code %}{% raw %} - -… -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
- -

Dark mode

-

To enable dark mode add the pos-theme-darkEnabled class to the same container. Dark theme setting is based on the user's system settings or can be forced by adding a pos-theme-dark class to your app container.

-{% capture code %}{% raw %} - -… -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
-
Initialize common styling class
pos-app
-
Enable automatic dark mode class
pos-theme-darkEnabled
-
Manually turn on dark theme class
pos-theme-dark
-
-
-
- - - -
-

Colors

- -

General content

-
    -
  • -

    Page background

    -
    -
    -
    - --pos-color-page-background -
    - # -
    -
    -
  • -
  • -

    Content background

    -
    -
    -
    - --pos-color-content-background -
    - # -
    -
    -
  • -
  • -

    Content text & icons

    -
    -
    -
    --pos-color-content-text
    - # -
    -
    -
    --pos-color-content-icon
    - # -
    -
    -
    --pos-color-content-text-supplementary
    - # -
    -
    -
    --pos-color-content-text-prominent
    - # -
    -
    -
  • -
  • -

    Borders & separators

    -
    -
    -
    --pos-color-frame
    - # -
    -
    -
  • -
  • -

    Highlighted elements

    -
    -
    -
    --pos-color-highlight-background
    - # -
    -
    -
    --pos-color-highlight-text
    - # -
    -
    -
  • -
  • -

    Standout sections, call to actions

    -
    -
    -
    --pos-color-standout-background
    - # -
    -
    -
    --pos-color-standout-background-hover
    - # -
    -
    -
    --pos-color-standout-text
    - # -
    -
    -
  • -
- -

Interactive elements

-
    -
  • -

    Links

    -
    -
    -
    --pos-color-interactive
    - # -
    -
    -
    --pos-color-interactive-hover
    - # -
    -
    -
    --pos-color-interactive-active
    - # -
    -
    -
    --pos-color-interactive-disabled
    - # -
    -
    -
  • - -
  • -

    Primary buttons

    -
    -
    -
    --pos-color-button-primary-background
    - # -
    -
    -
    --pos-color-button-primary-frame
    - # -
    -
    -
    --pos-color-button-primary-text
    - # -
    -
    -
    -
    -
    --pos-color-button-primary-hover-background
    - # -
    -
    -
    --pos-color-button-primary-hover-frame
    - # -
    -
    -
    --pos-color-button-primary-hover-text
    - # -
    -
    -
    -
    -
    --pos-color-button-primary-active-background
    - # -
    -
    -
    --pos-color-button-primary-active-frame
    - # -
    -
    -
    --pos-color-button-primary-active-text
    - # -
    -
    -
    -
    -
    --pos-color-button-primary-disabled-background
    - # -
    -
    -
    --pos-color-button-primary-disabled-frame
    - # -
    -
    -
    --pos-color-button-primary-disabled-text
    - # -
    -
    -
  • - -
  • -

    Secondary buttons

    -
    -
    -
    --pos-color-button-secondary-background
    - # -
    -
    -
    --pos-color-button-secondary-frame
    - # -
    -
    -
    --pos-color-button-secondary-text
    - # -
    -
    -
    -
    -
    --pos-color-button-secondary-hover-background
    - # -
    -
    -
    --pos-color-button-secondary-hover-frame
    - # -
    -
    -
    --pos-color-button-secondary-hover-text
    - # -
    -
    -
    -
    -
    --pos-color-button-secondary-active-background
    - # -
    -
    -
    --pos-color-button-secondary-active-frame
    - # -
    -
    -
    --pos-color-button-secondary-active-text
    - # -
    -
    -
    -
    -
    --pos-color-button-secondary-disabled-background
    - # -
    -
    -
    --pos-color-button-secondary-disabled-frame
    - # -
    -
    -
    --pos-color-button-secondary-disabled-text
    - # -
    -
    -
  • -
- -

Browser UI

-
    -
  • -

    Focused elements highlight

    -
    -
    -
    --pos-color-focused
    - # -
    -
    -
  • -
  • -

    Text selection highlight

    -
    -
    -
    --pos-color-selection-background
    - # -
    -
    -
    --pos-color-selection-text
    - # -
    -
    -
  • -
- -

Forms

-
    -
  • -

    Placeholder text

    -
    -
    -
    --pos-color-input-placeholder
    - # -
    -
    -
  • -
  • -

    Input field

    -
    -
    -
    --pos-color-input-background
    - # -
    -
    -
    --pos-color-input-frame
    - # -
    -
    -
    --pos-color-input-text
    - # -
    -
    -
    -
    -
    --pos-color-input-hover-background
    - # -
    -
    -
    --pos-color-input-hover-frame
    - # -
    -
    -
    --pos-color-input-hover-text
    - # -
    -
    -
    -
    -
    --pos-color-input-active-background
    - # -
    -
    -
    --pos-color-input-active-frame
    - # -
    -
    -
    --pos-color-input-active-text
    - # -
    -
    -
    -
    -
    --pos-color-input-disabled-background
    - # -
    -
    -
    --pos-color-input-disabled-frame
    - # -
    -
    -
    --pos-color-input-disabled-text
    - # -
    -
    -
  • -
- -

Utility

-
    -
  • -

    Statuses

    -
    -
    -
    --pos-color-important
    - # -
    -
    -
    --pos-color-important-hover
    - # -
    -
    -
    --pos-color-important-disabled
    - # -
    -
    -
    -
    -
    --pos-color-warning
    - # -
    -
    -
    --pos-color-warning-hover
    - # -
    -
    -
    --pos-color-warning-disabled
    - # -
    -
    -
    -
    -
    --pos-color-confirmation
    - # -
    -
    -
    --pos-color-confirmation-hover
    - # -
    -
    -
    --pos-color-confirmation-disabled
    - # -
    -
    -
  • -
- -
- - - -
-

Gradients and shadows

- -

Increasing text legibility over na image

-

When using text on an image you might need to increase the text legibility and ensure the contrast between those two stays high. For that you may use an eased gradient available through the css custom property: --pos-gradient-legibility or a pre-defined class pos-increaseLegibility. Be caresul with the later though as it uses relative positioning and may break your layout in some cases.

-
-
Class
pos-increaseLegibility
-
Properties
--pos-gradient-legibility
-
- - -

The quick brown fox

-
-
- - - -
-

Icons

-{% capture code %}{% raw %} -{% render 'modules/common-styling/icon', icon: 'dashDown' %} -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
    - {% render 'modules/common-styling/icon', icon: 'all' %} -
-
- - - -
-

Fonts

- -
-
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. In euismod aliquet nisi euismod eleifend. Phasellus justo tellus, aliquet ac aliquam ut, dictum eu augue.

-

Nullam vitae ex sed ligula convallis suscipit. Maecenas et neque facilisis.

- - - Aa -
    -
  • Light
  • -
  • Regular
  • -
  • Medium
  • -
  • Semi Bold
  • -
  • Bold
  • -
- -
-
-
Property
--pos-font-default
-
Font family
-
Default font size
-
-
- -
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. In euismod aliquet nisi euismod eleifend. Phasellus justo tellus, aliquet ac aliquam ut, dictum eu augue.

-

Nullam vitae ex sed ligula convallis suscipit. Maecenas et neque facilisis.

- - - Aa -
    -
  • Light
  • -
  • Regular
  • -
  • Medium
  • -
  • Semi Bold
  • -
  • Bold
  • -
- -
-
-
Property
--pos-font-heading
-
Font family
-
Default font size
-
-
-
- -
- - - -
-

Headings

- -

Heading 1

-
-
-
Class
pos-heading-1
-
Font family
-
Color
-
Size
-
Weight
-
Line height
-
-
- - The quick brown fox jumps over the lazy dog - -{% capture code %}{% raw %} -

The quick brown fox jumps over the lazy dog

-{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
- -

Heading 2

-
-
-
Class
pos-heading-2
-
Font family
-
Color
-
Size
-
Weight
-
Line height
-
-
- - The quick brown fox jumps over the lazy dog - -{% capture code %}{% raw %} -

The quick brown fox jumps over the lazy dog

-{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
- -

Heading 3

-
-
-
Class
pos-heading-3
-
Font family
-
Color
-
Size
-
Weight
-
Line height
-
-
- - The quick brown fox jumps over the lazy dog - -{% capture code %}{% raw %} -

The quick brown fox jumps over the lazy dog

-{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
- -

Heading 4

-
-
-
Class
pos-heading-4
-
Font family
-
Color
-
Size
-
Weight
-
Line height
-
-
- - The quick brown fox jumps over the lazy dog - -{% capture code %}{% raw %} -

The quick brown fox jumps over the lazy dog

-{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
- -
- - - -
-

Text styles

-

Sidenote

-
-
-
Class
pos-supplementary
-
Font family
-
Color
-
Size
-
Weight
-
Line height
-
-
- - The quick brown fox jumps over the lazy dog - -{% capture code %}{% raw %} -The quick brown fox jumps over the lazy dog -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
- -

Tip

-
-
-
Class
pos-tip
-
Font family
-
Color
-
Size
-
Weight
-
Line height
-
-
- - {% render 'modules/common-styling/tip', content: 'The quick brown fox jumps over the lazy dog' %} - - {% capture code %}{% raw %} - {% render 'modules/common-styling/tip', content: 'The quick brown fox jumps over the lazy dog' %} - {% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
- - -
-

Links

- -
- -
- - {% render 'modules/common-styling/tip', content: 'When overwriting the <a> classes, please remember to also overwrite the debug classes used in the style guide: pos-debug-link-hover, pos-debug-link-active, pos-debug-link-focus.' %} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TextIconText with icon
DefaultLink Link Link
HoverLink Link Link
ActiveLink Link Link
FocusedLink Link Link
-
- - - - -
- -

Buttons

- -
-
-
-

Default

- - - -
-
Class
pos-button
-
-
-{% capture code %}{% raw %} - -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-

Primary

- - - -
-
Class
pos-button pos-button-primary
-
-
-{% capture code %}{% raw %} - -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
- -
-
-

Default small

- - - -
-
Class
pos-button pos-button-small
-
-
-{% capture code %}{% raw %} - -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-

Primary small

- - - -
-
Class
pos-button pos-button-small pos-button-small
-
-
-{% capture code %}{% raw %} - -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
- - {% render 'modules/common-styling/tip', content: 'When overwriting the <button> classes, please remember to also overwrite the debug classes used in the style guide: pos-debug-button-hover, pos-debug-button-active, pos-debug-button-focus-visible.' %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DefaultPrimaryDefault smallPrimary small
Enabled
Hover
Active
Focused
Disabled
Icon - - - - - - - -
LinkLinkLinkLinkLink
- -
- - - -
-

Forms

-

There are two ways for styling form controlls. You can add a pos-form class to a container and make all the child inputs styled automatically or you can add one of the following classes to any single input to style it separately.

- -

Basic example

-
-
- - -
-
- - -
-
- - -
-
- - -
-
-{% capture code %}{% raw %} -
-
- - - {% render 'modules/common-styling/forms/error_list', name: 'styleguide-example-error', errors: errors['styleguide-form-example-a'] %} -
-
- - - {% render 'modules/common-styling/forms/error_list', name: 'styleguide-example-error', errors: errors['styleguide-form-example-b'] %} -
-
- - - {% render 'modules/common-styling/forms/error_list', name: 'styleguide-example-error', errors: errors['styleguide-form-example-c'] %} -
-
-{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
- - -

Containers

- -
- -
-
-
-
-
-
-
-
-
Class
pos-form
-
-

Used for complex forms that needs more manual customized styling. No automatic labels styling, no automatic spacing between elements.

-
- -
-
-
-
-
-
-
-
Class
pos-form pos-form-simple
-
-

Used for simple forms that can be styled automatically. Styles the labels and spacing between items as well. You can just throw this class onto the container and forget about styling each separate control.

-
- -
- -

Rows

- -
-
-
-
Class
pos-form-fieldset
-
Properties
--pos-gap-text-text
-
-{% capture code %}{% raw %} -
- -
-{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
-
-
-
-
- -
-
-
-
Class
pos-form-fieldset-combined
-
-{% capture code %}{% raw %} -
- -
-{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
-
-
-
-
-
-
- -

Labels

- -
- {% render 'modules/common-styling/tip', content: 'Labels that are placed in a fieldset that has a reqired input will automatically be marked with an asterisk.' %} -
-
-
-
- - -
-
-
- {% capture code %}{% raw %} - - {% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
- -
- -
-

Radio

- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- -
-

Checkbox

- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- -
- - -

Text inputs

- -
- -
-
- -
-
Class
pos-form-input
-
-
-{% capture code %}{% raw %} - -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
- - {% render 'modules/common-styling/tip', content: 'When overwriting the <input> classes, please remember to also overwrite the debug classes used in the style guide: pos-debug-form-input-hover, pos-debug-form-input-focus-visible.' %} -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PlaceholderFilled
Default
Hover
Focused
Disabled
Error
-
- -
- -
- -
-
- -
-
Class
pos-form-input
-
-
- {% capture code %}{% raw %} - - {% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
- - {% render 'modules/common-styling/tip', content: 'When overwriting the <input> classes, please remember to also overwrite the debug classes used in the style guide: pos-debug-form-input-hover, pos-debug-form-input-focus-visible.' %} -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PlaceholderFilled
Default
Hover
Focused
Disabled
Error
-
-
- - -

Password input

- -
-
-
-
name
Input name attribute string
-
id
Input id attribute string
-
value
Current input value string
-
class
Class list added to input container string
-
meter
If you want to show the password strength meter bool
-
- {% render 'modules/common-styling/tip', content: 'Strong passwords consists of small and capitalized letters, numbers, special signs and are at least 6 characters long. Remember to provide clear instructios for your users.' %} -
-
-
- {% render 'modules/common-styling/forms/password', name: 'styleguide-form-password-test', id: 'styleguide-form-password-test', value: '123456', meter: true %} -
-{% capture code %}{% raw %} -{% render 'modules/common-styling/forms/password', - name: 'styleguide-form-password-test', - value: '123', - id: 'styleguide-form-password-test', - meter: true -%} -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
- - -

Select

- -
-
-
-
class
pos-form-select
-
-
-
- - - -
-
- - -

Multiselect

- -
-
-
-
id
Unique ID for the input string
-
list
-
- an array of objects with items to show, must include 'value' and 'label' array
- [ { value: 'item1value', label: 'Item 1 label' }, { value: 'item2value', label: 'Item 2 label' } ] -
-
selected
-
- array with selected values (the same as in the 'list') array
- [ 'item2value' ] -
-
form
the <form> element that the multiselect corresponds to string
-
name
the name="" property for the multiselect checkboxes string
-
required
at least one option is required bool
-
combine_selected
if you want to combine selected items into a single element ('2 selected' instead of displaying names) bool
-
multiline
if you want the list to extend vertically if there are more items than fit the single line bool
-
showFilter
allow to filter the list of options with a text input bool
-
placeholder
translation key for the main select input placeholder string
-
placeholder_filter
translation key for the filter input placeholder string
-
placeholder_empty
translation key shown when the filter brings no results string
-
-
-
-
- {% liquid - assign example_list = '' | split: '' - - for i in (i..10) - assign value = 'value' | append: i - assign label = 'Label for value ' | append: i - assign example_item = '{}' | parse_json | hash_merge: value: value, label: label - assign example_list = example_list | add_to_array: example_item - assign selected = '["value0", "value5", "value6"]' | parse_json - endfor - %} - {% render 'modules/common-styling/forms/multiselect', name: 'styleguide-form-multiselect-test-1', id: 'styleguide-form-multiselect-test-1', list: example_list, showFilter: true, combine_selected: true %} - {% render 'modules/common-styling/forms/multiselect', name: 'styleguide-form-multiselect-test-2', id: 'styleguide-form-multiselect-test-2', list: example_list, showFilter: true %} - {% render 'modules/common-styling/forms/multiselect', name: 'styleguide-form-multiselect-test-3', id: 'styleguide-form-multiselect-test-3', list: example_list, showFilter: false, multiline: true %} - {% render 'modules/common-styling/forms/multiselect', name: 'styleguide-form-multiselect-test-4', id: 'styleguide-form-multiselect-test-4', list: example_list, selected: selected, showFilter: true, combine_selected: true %} -
-{% capture code %}{% raw %} -{% render 'modules/common-styling/forms/multiselect', - name: 'styleguide-form-multiselect-test', - id: 'styleguide-form-multiselect-test' -%} -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
- - -

Error handling

- -

There are two partials that can be helpful when dealing with forms validation. One can be added to the input itself to handle usability code and the other can output the error message.

- - {% liquid - assign errors = '{ "styleguide-example-error": ["This is a field with two errors", "This is the second error"] }' | parse_json - %} -
- - {% render 'modules/common-styling/forms/error_list', name: 'styleguide-example-error', errors: errors['styleguide-example-error'] %} -{% capture code %}{% raw %} - - -{% render 'modules/common-styling/forms/error_list', name: 'styleguide-example-error', errors: errors['styleguide-example-error'] %} -{% endraw %}{% endcapture %} -
-
-
{{ code | lstrip | rstrip }}
-
- - -
- - - - -
-

Boxes

- -
-
-
-
The quick brown fox jumps over the lazy dog
-
- {% capture code %}{% raw %} -
- {% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
class
pos-card
-
props
--pos-padding-card, --pos-radius-card, --pos-color-content-background
-
-
-
-
-
The quick brown fox jumps over the lazy dog
-
- {% capture code %}{% raw %} -
- {% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
class
pos-card pos-card-highlighted
-
props
--pos-padding-card, --pos-radius-card, --pos-color-highlight-background
-
-
-
- -

Content card

-
-
- {% render 'modules/common-styling/content/card', url: '/', image: 'https://picsum.photos/1000/400', title: 'Lorem ipsum dolor sit amet', content: 'Quisque vel velit mi. Proin malesuada iaculis viverra. Vestibulum tristique sollicitudin rhoncus. Vivamus sollicitudin nisi in lorem gravida aliquam.', footer: '
  • Item
  • Item
Aside item' %} -{% capture code %}{% raw %} -{% render 'modules/common-styling/content/card', url: '/', image: 'https://picsum.photos/1000/400', title: 'Title', content: 'Content', footer: '
  • Item
  • Item
Aside item' %} -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
- {% render 'modules/common-styling/content/card', url: '/', image: 'https://picsum.photos/1000/400?random=2', title: 'Lorem ipsum dolor sit amet', content: 'Quisque vel velit mi. Proin malesuada iaculis viverra. Vestibulum tristique sollicitudin rhoncus. Vivamus sollicitudin nisi in lorem gravida aliquam.', footer: 'Cras lacinia lorem', highlighted: true %} -{% capture code %}{% raw %} -{% render 'modules/common-styling/content/card', url: '/', image: 'https://picsum.photos/1000/400', title: 'Title', content: 'Content', footer: 'Footer', highlighted: true %} -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
-
- - - -
-

Tables

- -
-
-{% capture code %}{% raw %} -
-
-
Column 1
-
Column 2
-
Column 3
-
-
-
    -
  • - Column 1 - Content 1 -
  • -
  • - Column 1 - Content 2 -
  • -
  • - Column 3 - 321 -
  • -
-
-
-{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
-
-
-
-
Column 1
-
Column 2
-
Column 3
-
-
-
    -
  • - Column 1 - Content 1 -
  • -
  • - Column 1 - Content 2 -
  • -
  • - Column 3 - 321 -
  • -
-
    -
  • - Column 1 - Content 2 -
  • -
  • - Column 2 - Content 2 -
  • -
  • - Column 3 - 123 -
  • -
-
-
-
-
-
class
pos-table
-
props
--pos-padding-cell
-
-
-
- -
-
-{% capture code %}{% raw %} -
-
-
Column 1
-
Column 2
-
Column 3
-
-
-
    -
  • - Column 1 - Content 1 -
  • -
  • - Column 1 - Content 2 -
  • -
  • - Column 3 - 321 -
  • -
-
-
-{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
-
-
-
-
-
-
Column 1
-
Column 2
-
Column 3
-
-
-
    -
  • - Column 1 - Content 1 -
  • -
  • - Column 1 - Content 2 -
  • -
  • - Column 3 - 321 -
  • -
-
    -
  • - Column 1 - Content 2 -
  • -
  • - Column 2 - Content 2 -
  • -
  • - Column 3 - 123 -
  • -
-
-
-
-
-
class
pos-table
-
props
--pos-padding-cell
-
-
-
- -
- - - -
-

Toasts

- -
-
-

A standard platformOS way of showing toast notifications would be to store and get the messages in the session.

-

Adding the following code to your application `layout` file will initialize the module:

-{% capture code %}{% raw %} -{% liquid - function flash = 'modules/core/commands/session/get', key: 'sflash' - if context.location.pathname != flash.from or flash.force_clear - function _ = 'modules/core/commands/session/clear', key: 'sflash' - endif - theme_render_rc 'modules/common-styling/toasts', params: flash -%} -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
- -

Then, you can use the following JavaScript to show a message on page:

-{% capture code %}{% raw %} -new pos.modules.toast('[severity]', '[message]'); -{% endraw %}{% endcapture %} -
-
{{ code | lstrip | rstrip }}
-
- -
-
severity
how important the message is - error, success, info string
-
message
user-readable message for the toast notification string
-
-
-
-
- -
-
- -
-
- -
-
-
-
- - - - - + {% liquid + render 'modules/common-styling/style-guide/initialization' + render 'modules/common-styling/style-guide/colors' + render 'modules/common-styling/style-guide/gradients' + render 'modules/common-styling/style-guide/icons' + render 'modules/common-styling/style-guide/spacings' + render 'modules/common-styling/style-guide/fonts' + render 'modules/common-styling/style-guide/headings' + render 'modules/common-styling/style-guide/text-styles' + render 'modules/common-styling/style-guide/links' + render 'modules/common-styling/style-guide/buttons' + render 'modules/common-styling/style-guide/forms' + render 'modules/common-styling/style-guide/boxes' + render 'modules/common-styling/style-guide/tables' + render 'modules/common-styling/style-guide/toasts' + render 'modules/common-styling/style-guide/tags' + render 'modules/common-styling/style-guide/navigation' + %}
-
- -
- -
- The quick brown fox jumps over the lazy dog -
-
- -
- -
- The quick brown fox jumps over the lazy dog -
-
\ No newline at end of file diff --git a/modules/common-styling/public/views/partials/content/alert.liquid b/modules/common-styling/public/views/partials/content/alert.liquid new file mode 100644 index 0000000..062fdbf --- /dev/null +++ b/modules/common-styling/public/views/partials/content/alert.liquid @@ -0,0 +1,33 @@ +{% comment %} + + alert box + + type - (string) one of 'success', 'error', 'warning', 'info' (default: 'info') + content - (string) card description + +{% endcomment %} + + +{% liquid + + assign type = type | default: 'info' + +%} + + +
+ {% case type %} + {% when 'info' %} + {% render 'modules/common-styling/icon', icon: 'info' %} + {% when 'warning' %} + {% render 'modules/common-styling/icon', icon: 'warning' %} + {% when 'error' %} + {% render 'modules/common-styling/icon', icon: 'delete' %} + {% when 'success' %} + {% render 'modules/common-styling/icon', icon: 'checkBadge' %} + {% endcase %} + + + {{ content | html_safe }} + +
diff --git a/modules/common-styling/public/views/partials/content/dialog.liquid b/modules/common-styling/public/views/partials/content/dialog.liquid new file mode 100644 index 0000000..cb045bc --- /dev/null +++ b/modules/common-styling/public/views/partials/content/dialog.liquid @@ -0,0 +1,24 @@ +{% comment %} + + modal dialog that is hidden by default and can be shown with a button + + id - (string) unique ID for the dialog + title - (string) dialog title + content - (string) html content for the dialog + +{% endcomment %} + + + + +
+ {% if title %} +

{{ title }}

+ {% endif %} + +
+ {% print content %} +
\ No newline at end of file diff --git a/modules/common-styling/public/views/partials/icon.liquid b/modules/common-styling/public/views/partials/icon.liquid index 2219439..3b1e281 100644 --- a/modules/common-styling/public/views/partials/icon.liquid +++ b/modules/common-styling/public/views/partials/icon.liquid @@ -9,6 +9,10 @@ {% endcomment %} +{% liquid + assign class = class | default: '' +%} + {% capture attrs %} viewBox="0 0 24 24" fill="none" @@ -39,6 +43,9 @@ {% when 'dashLeft', 'all' %} + {% when 'pencil', 'all' %} + + {% when 'check', 'all' %} @@ -99,9 +106,39 @@ {% when 'bookmarksDocument', 'all' %} + {% when 'info', 'all' %} + + + {% when 'warning', 'all' %} + + + {% when 'delete', 'all' %} + + + {% when 'checkBadge', 'all' %} + + {% when 'leave', 'all' %} + {% when 'location', 'all' %} + + + {% when 'globe', 'all' %} + + + {% when 'clock', 'all' %} + + + {% when 'crown', 'all' %} + + + {% when 'crownRotated', 'all' %} + + + {% when 'linkedin', 'all' %} + + diff --git a/modules/common-styling/public/views/partials/init.liquid b/modules/common-styling/public/views/partials/init.liquid index 126f0fb..2771073 100644 --- a/modules/common-styling/public/views/partials/init.liquid +++ b/modules/common-styling/public/views/partials/init.liquid @@ -2,6 +2,7 @@ {% endif %} + @@ -9,6 +10,7 @@ + @@ -66,6 +68,18 @@ }); }; + /* modals */ + if(document.querySelector('[data-dialogtarget]')){ + import('{{ 'modules/common-styling/js/pos-dialog.js' | asset_url }}').then(module => { + document.querySelectorAll('[data-dialogtarget]').forEach((element, index) => { + window.pos.modules.active[element.dataset.dialogtarget] = new window.pos.modules.dialog({ + trigger: element, + container: document.querySelector('#' + element.dataset.dialogtarget) + }); + }); + }); + }; + // purpose: loads HTML from an endpoint and puts it in the container // arguments: endpoint (string) - URL to load the content from // target (string) - selector of the element where the content should be loaded diff --git a/modules/common-styling/public/views/partials/pagination.liquid b/modules/common-styling/public/views/partials/pagination.liquid index 99bcd43..4865dff 100644 --- a/modules/common-styling/public/views/partials/pagination.liquid +++ b/modules/common-styling/public/views/partials/pagination.liquid @@ -1,79 +1,61 @@ -{% comment %} +{% comment %} numbered pagination with arrows if the number of pages is large - count - (int) number of pages in total - maxVisible - (int) maximum number of pages to show on the list, more will can be navigated with arrows - active - (int) currently viewed page - url - (string) query string for the pagination + total pages - (int) how many total pages available {% endcomment %} - {% liquid - assign count = count | default: 10 - assign maxVisible = maxVisible | default: 10 - assign url = url | default: '?page=' - - if count > maxVisible - assign limit = maxVisible | divided_by: 2 | round - assign startPage = active | minus: limit - assign endPage = active | plus: limit - endif - - if count <= maxVisible - assign startPage = 1 - assign endPage = count - endif - - if startPage < 1 - assign startPage = 1 - assign endPage = maxVisible - endif - - if endPage > count - assign startPage = count | minus: maxVisible | plus: 1 - assign endPage = count + assign current = context.location.search.page | to_positive_integer: 1 + + assign url = '?' + if context.location.search + assign query_string = context.location.search + assign removed = query_string | hash_delete_key: 'page' + + if query_string.size > 0 + assign query_string = query_string | querify + assign url = url | append: query_string | append: '&page=' + else + assign url = url | append: 'page=' + endif endif - - assign leftArrow = startPage | minus: 1 - assign rightArrow = endPage | plus: 1 %} +{% if total_pages > 1 %} - - +{% endif %} diff --git a/modules/common-styling/public/views/partials/style-guide/boxes.liquid b/modules/common-styling/public/views/partials/style-guide/boxes.liquid new file mode 100644 index 0000000..f12976a --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/boxes.liquid @@ -0,0 +1,58 @@ +
+

Boxes

+ +
+
+
+
The quick brown fox jumps over the lazy dog
+
+{% capture code %}{% raw %} +
+{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
class
pos-card
+
props
--pos-padding-card, --pos-radius-card, --pos-color-content-background
+
+
+
+
+
The quick brown fox jumps over the lazy dog
+
+{% capture code %}{% raw %} +
+{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
class
pos-card pos-card-highlighted
+
props
--pos-padding-card, --pos-radius-card, --pos-color-highlight-background
+
+
+
+ +

Content card

+
+
+ {% render 'modules/common-styling/content/card', url: '/', image: 'https://picsum.photos/1000/400', title: 'Lorem ipsum dolor sit amet', content: 'Quisque vel velit mi. Proin malesuada iaculis viverra. Vestibulum tristique sollicitudin rhoncus. Vivamus sollicitudin nisi in lorem gravida aliquam.', footer: '
  • Item
  • Item
Aside item', highlighted: null %} +{% capture code %}{% raw %} +{% render 'modules/common-styling/content/card', url: '/', image: 'https://picsum.photos/1000/400', title: 'Title', content: 'Content', footer: '
  • Item
  • Item
Aside item' %} +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+ {% render 'modules/common-styling/content/card', url: '/', image: 'https://picsum.photos/1000/400?random=2', title: 'Lorem ipsum dolor sit amet', content: 'Quisque vel velit mi. Proin malesuada iaculis viverra. Vestibulum tristique sollicitudin rhoncus. Vivamus sollicitudin nisi in lorem gravida aliquam.', footer: 'Cras lacinia lorem', highlighted: true %} +{% capture code %}{% raw %} +{% render 'modules/common-styling/content/card', url: '/', image: 'https://picsum.photos/1000/400', title: 'Title', content: 'Content', footer: 'Footer', highlighted: true %} +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+
diff --git a/modules/common-styling/public/views/partials/style-guide/buttons.liquid b/modules/common-styling/public/views/partials/style-guide/buttons.liquid new file mode 100644 index 0000000..8be3dab --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/buttons.liquid @@ -0,0 +1,158 @@ +
+ +

Buttons

+ +
+
+
+

Default

+ + + +
+
Class
pos-button
+
+
+{% capture code %}{% raw %} + +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+

Primary

+ + + +
+
Class
pos-button pos-button-primary
+
+
+{% capture code %}{% raw %} + +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+ +
+
+

Default small

+ + + +
+
Class
pos-button pos-button-small
+
+
+{% capture code %}{% raw %} + +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+

Primary small

+ + + +
+
Class
pos-button pos-button-small pos-button-small
+
+
+{% capture code %}{% raw %} + +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+ + {% render 'modules/common-styling/tip', content: 'When overwriting the <button> classes, please remember to also overwrite the debug classes used in the style guide: pos-debug-button-hover, pos-debug-button-active, pos-debug-button-focus-visible.' %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DefaultPrimaryDefault smallPrimary small
Enabled
Hover
Active
Focused
Disabled
Icon + + + + + + + +
LinkLinkLinkLinkLink
+ +
diff --git a/modules/common-styling/public/views/partials/style-guide/colors.liquid b/modules/common-styling/public/views/partials/style-guide/colors.liquid new file mode 100644 index 0000000..46819fe --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/colors.liquid @@ -0,0 +1,382 @@ +
+

Colors

+ +

General content

+ + +

Interactive elements

+ + +

Browser UI

+ + +

Forms

+ + +

Utility

+ + +
diff --git a/modules/common-styling/public/views/partials/style-guide/fonts.liquid b/modules/common-styling/public/views/partials/style-guide/fonts.liquid new file mode 100644 index 0000000..9cb7579 --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/fonts.liquid @@ -0,0 +1,68 @@ +
+

Fonts

+ +
+
+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. In euismod aliquet nisi euismod eleifend. Phasellus justo tellus, aliquet ac aliquam ut, dictum eu augue.

+

Nullam vitae ex sed ligula convallis suscipit. Maecenas et neque facilisis.

+
+ + + Aa +
    +
  • Light
  • +
  • Regular
  • +
  • Medium
  • +
  • Semi Bold
  • +
  • Bold
  • +
+ +
+
+
Property
--pos-font-default
+
Font family
+
Default font size
+
+
+ +
+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. In euismod aliquet nisi euismod eleifend. Phasellus justo tellus, aliquet ac aliquam ut, dictum eu augue.

+

Nullam vitae ex sed ligula convallis suscipit. Maecenas et neque facilisis.

+
+ + + Aa +
    +
  • Light
  • +
  • Regular
  • +
  • Medium
  • +
  • Semi Bold
  • +
  • Bold
  • +
+ +
+
+
Property
--pos-font-heading
+
Font family
+
Default font size
+
+
+
+ +
diff --git a/modules/common-styling/public/views/partials/style-guide/forms.liquid b/modules/common-styling/public/views/partials/style-guide/forms.liquid new file mode 100644 index 0000000..17cea18 --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/forms.liquid @@ -0,0 +1,469 @@ +
+

Forms

+

There are two ways for styling form controlls. You can add a pos-form class to a container and make all the child inputs styled automatically or you can add one of the following classes to any single input to style it separately.

+ +

Basic example

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+{% capture code %}{% raw %} +
+
+ + + {% render 'modules/common-styling/forms/error_list', name: 'styleguide-example-error', errors: errors['styleguide-form-example-a'] %} +
+
+ + + {% render 'modules/common-styling/forms/error_list', name: 'styleguide-example-error', errors: errors['styleguide-form-example-b'] %} +
+
+ + + {% render 'modules/common-styling/forms/error_list', name: 'styleguide-example-error', errors: errors['styleguide-form-example-c'] %} +
+
+{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+ + +

Containers

+ +
+ +
+
+
+
+
+
+
+
+
Class
pos-form
+
+

Used for complex forms that needs more manual customized styling. No automatic labels styling, no automatic spacing between elements.

+
+ +
+
+
+
+
+
+
+
Class
pos-form pos-form-simple
+
+

Used for simple forms that can be styled automatically. Styles the labels and spacing between items as well. You can just throw this class onto the container and forget about styling each separate control.

+
+ +
+ +

Rows

+ +
+
+
+
Class
pos-form-fieldset
+
Properties
--pos-gap-text-text
+
+{% capture code %}{% raw %} +
+ +
+{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+
+
+
+
+ +
+
+
+
Class
pos-form-fieldset-combined
+
+{% capture code %}{% raw %} +
+ +
+{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+
+
+
+
+
+
+ +

Form actions

+
+
+
+
+
+
+{% capture code %}{% raw %} +
+ +
+{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+ + +

Labels

+ +
+ {% render 'modules/common-styling/tip', content: 'Labels that are placed in a fieldset that has a reqired input will automatically be marked with an asterisk.' %} +
+
+
+
+ + +
+
+
+ {% capture code %}{% raw %} + + {% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+ +
+ +
+

Radio

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+

Checkbox

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + +

Text inputs

+ +
+ +
+
+ +
+
Class
pos-form-input
+
+
+{% capture code %}{% raw %} + +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+ + {% render 'modules/common-styling/tip', content: 'When overwriting the <input> classes, please remember to also overwrite the debug classes used in the style guide: pos-debug-form-input-hover, pos-debug-form-input-focus-visible.' %} +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PlaceholderFilled
Default
Hover
Focused
Disabled
Error
+
+ +
+ +
+ +
+
+ +
+
Class
pos-form-input
+
+
+ {% capture code %}{% raw %} + + {% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+ + {% render 'modules/common-styling/tip', content: 'When overwriting the <input> classes, please remember to also overwrite the debug classes used in the style guide: pos-debug-form-input-hover, pos-debug-form-input-focus-visible.' %} +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PlaceholderFilled
Default
Hover
Focused
Disabled
Error
+
+
+ + +

Password input

+ +
+
+
+
name
Input name attribute string
+
id
Input id attribute string
+
value
Current input value string
+
class
Class list added to input container string
+
meter
If you want to show the password strength meter bool
+
+ {% render 'modules/common-styling/tip', content: 'Strong passwords consists of small and capitalized letters, numbers, special signs and are at least 6 characters long. Remember to provide clear instructios for your users.' %} +
+
+
+ {% render 'modules/common-styling/forms/password', name: 'styleguide-form-password-test', id: 'styleguide-form-password-test', value: '123456', meter: true, class: null %} +
+{% capture code %}{% raw %} +{% render 'modules/common-styling/forms/password', + name: 'styleguide-form-password-test', + value: '123', + id: 'styleguide-form-password-test', + meter: true +%} +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+ + +

Select

+ +
+
+
+
class
pos-form-select
+
+
+
+ + + +
+
+ + +

Multiselect

+ +
+
+
+
id
Unique ID for the input string
+
list
+
+ an array of objects with items to show, must include 'value' and 'label' array
+ [ { value: 'item1value', label: 'Item 1 label' }, { value: 'item2value', label: 'Item 2 label' } ] +
+
selected
+
+ array with selected values (the same as in the 'list') array
+ [ 'item2value' ] +
+
form
the <form> element that the multiselect corresponds to string
+
name
the name="" property for the multiselect checkboxes string
+
required
at least one option is required bool
+
combine_selected
if you want to combine selected items into a single element ('2 selected' instead of displaying names) bool
+
multiline
if you want the list to extend vertically if there are more items than fit the single line bool
+
showFilter
allow to filter the list of options with a text input bool
+
placeholder
translation key for the main select input placeholder string
+
placeholder_filter
translation key for the filter input placeholder string
+
placeholder_empty
translation key shown when the filter brings no results string
+
+
+
+
+ {% liquid + assign example_list = '' | split: '' + + for i in (i..10) + assign value = 'value' | append: i + assign label = 'Label for value ' | append: i + assign example_item = '{}' | parse_json | hash_merge: value: value, label: label + assign example_list = example_list | add_to_array: example_item + assign selected = '["value0", "value5", "value6"]' | parse_json + endfor + %} + {% render 'modules/common-styling/forms/multiselect', name: 'styleguide-form-multiselect-test-1', id: 'styleguide-form-multiselect-test-1', list: example_list, showFilter: true, combine_selected: true, selected: selected, required: null, multiline: null, form: null %} + {% render 'modules/common-styling/forms/multiselect', name: 'styleguide-form-multiselect-test-2', id: 'styleguide-form-multiselect-test-2', list: example_list, showFilter: true, selected: selected, required: null, multiline: null, combine_selected: null, form: null %} + {% render 'modules/common-styling/forms/multiselect', name: 'styleguide-form-multiselect-test-3', id: 'styleguide-form-multiselect-test-3', list: example_list, showFilter: false, multiline: true, selected: selected, required: null, combine_selected: null, form: null %} + {% render 'modules/common-styling/forms/multiselect', name: 'styleguide-form-multiselect-test-4', id: 'styleguide-form-multiselect-test-4', list: example_list, selected: selected, showFilter: true, combine_selected: true, required: null, multiline: null, form: null %} +
+{% capture code %}{% raw %} +{% render 'modules/common-styling/forms/multiselect', + name: 'styleguide-form-multiselect-test', + id: 'styleguide-form-multiselect-test' +%} +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+ + +

Error handling

+ +

There are two partials that can be helpful when dealing with forms validation. One can be added to the input itself to handle usability code and the other can output the error message.

+ + {% liquid + assign errors = '{ "styleguide-example-error": ["This is a field with two errors", "This is the second error"] }' | parse_json + %} +
+ + {% render 'modules/common-styling/forms/error_list', name: 'styleguide-example-error', errors: errors['styleguide-example-error'] %} +{% capture code %}{% raw %} + + +{% render 'modules/common-styling/forms/error_list', name: 'styleguide-example-error', errors: errors['styleguide-example-error'] %} +{% endraw %}{% endcapture %} +
+
+
{{ code | lstrip | rstrip }}
+
+ + +
diff --git a/modules/common-styling/public/views/partials/style-guide/gradients.liquid b/modules/common-styling/public/views/partials/style-guide/gradients.liquid new file mode 100644 index 0000000..f677fe1 --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/gradients.liquid @@ -0,0 +1,14 @@ +
+

Gradients and shadows

+ +

Increasing text legibility over images

+

When placing text on top of an image, you may need to improve legibility and ensure the contrast stays high. You can achieve this with the eased gradient available through the CSS custom property --pos-gradient-legibility or by using the pre-defined class pos-increaseLegibility. Use the class with caution, as it relies on relative positioning and may affect your layout in some cases.

+
+
Class
pos-increaseLegibility
+
Properties
--pos-gradient-legibility
+
+ + +

The quick brown fox

+
+
diff --git a/modules/common-styling/public/views/partials/style-guide/headings.liquid b/modules/common-styling/public/views/partials/style-guide/headings.liquid new file mode 100644 index 0000000..6cea2d5 --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/headings.liquid @@ -0,0 +1,96 @@ +
+

Headings

+ +

Heading 1

+
+
+
Class
pos-heading-1
+
Font family
+
Color
+
Size
+
Weight
+
Line height
+
+
+ + The quick brown fox jumps over the lazy dog + +{% capture code %}{% raw %} +

The quick brown fox jumps over the lazy dog

+{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+ +

Heading 2

+
+
+
Class
pos-heading-2
+
Font family
+
Color
+
Size
+
Weight
+
Line height
+
+
+ + The quick brown fox jumps over the lazy dog + +{% capture code %}{% raw %} +

The quick brown fox jumps over the lazy dog

+{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+ +

Heading 3

+
+
+
Class
pos-heading-3
+
Font family
+
Color
+
Size
+
Weight
+
Line height
+
+
+ + The quick brown fox jumps over the lazy dog + +{% capture code %}{% raw %} +

The quick brown fox jumps over the lazy dog

+{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+ +

Heading 4

+
+
+
Class
pos-heading-4
+
Font family
+
Color
+
Size
+
Weight
+
Line height
+
+
+ + The quick brown fox jumps over the lazy dog + +{% capture code %}{% raw %} +

The quick brown fox jumps over the lazy dog

+{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+ +
diff --git a/modules/common-styling/public/views/partials/style-guide/icons.liquid b/modules/common-styling/public/views/partials/style-guide/icons.liquid new file mode 100644 index 0000000..cb3f910 --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/icons.liquid @@ -0,0 +1,12 @@ +
+

Icons

+{% capture code %}{% raw %} +{% render 'modules/common-styling/icon', icon: 'dashDown' %} +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+ +
diff --git a/modules/common-styling/public/views/partials/style-guide/initialization.liquid b/modules/common-styling/public/views/partials/style-guide/initialization.liquid new file mode 100644 index 0000000..15c7b4c --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/initialization.liquid @@ -0,0 +1,32 @@ +
+

Initialization

+
+
+

All of the following CSS (except CSS custom properties) are scoped to container that uses pos-app class. You can apply this class to the root html tag to style your entire app, or add it to a specific container to limit the scope.

+{% capture code %}{% raw %} + +… +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+ +

Dark mode

+

To enable dark mode, add the pos-theme-darkEnabled class to the same container. This will switch the theme automatically based on the user’s system settings. If you want to force dark mode manually, use the pos-theme-dark class instead.

+{% capture code %}{% raw %} + +… +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+
+
Initialize common styling class
pos-app
+
Enable automatic dark mode class
pos-theme-darkEnabled
+
Manually turn on dark theme class
pos-theme-dark
+
+
+
+
diff --git a/modules/common-styling/public/views/partials/style-guide/links.liquid b/modules/common-styling/public/views/partials/style-guide/links.liquid new file mode 100644 index 0000000..a9b3031 --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/links.liquid @@ -0,0 +1,47 @@ + diff --git a/modules/common-styling/public/views/partials/style-guide/navigation.liquid b/modules/common-styling/public/views/partials/style-guide/navigation.liquid new file mode 100644 index 0000000..d5444f4 --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/navigation.liquid @@ -0,0 +1,103 @@ + diff --git a/modules/common-styling/public/views/partials/style-guide/spacings.liquid b/modules/common-styling/public/views/partials/style-guide/spacings.liquid new file mode 100644 index 0000000..df6bda0 --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/spacings.liquid @@ -0,0 +1,31 @@ +
+ +

Spacings

+ +
+ +
+
+
+
+
+
+
Class
pos-gap-section-section, pos-mt-section-section
+
Properties
--pos-gap-section-section
+
+
+ +
+
+
+
+
+
+
Class
pos-gap-text-text, pos-mt-text-text
+
Properties
--pos-gap-text-text
+
+
+ +
+ +
\ No newline at end of file diff --git a/modules/common-styling/public/views/partials/style-guide/tables.liquid b/modules/common-styling/public/views/partials/style-guide/tables.liquid new file mode 100644 index 0000000..d6d9468 --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/tables.liquid @@ -0,0 +1,159 @@ +
+

Tables

+ +
+ {% capture code %}{% raw %} +
+
+
Column 1
+
Column 2
+
Column 3
+
+
+
    +
  • + Column 1 + Content 1 +
  • +
  • + Column 1 + Content 2 +
  • +
  • + Column 3 + 321 +
  • +
+
+
+ {% endraw %}{% endcapture %} +
+
+
{{ code | lstrip | rstrip }}
+
+
+
+
+
+
+
Column 1
+
Column 2
+
Column 3
+
+
+
    +
  • + Column 1 + Content 1 +
  • +
  • + Column 1 + Content 2 +
  • +
  • + Column 3 + 321 +
  • +
+
    +
  • + Column 1 + Content 2 +
  • +
  • + Column 2 + Content 2 +
  • +
  • + Column 3 + 123 +
  • +
+
+
+
+
+
class
pos-table
+
props
--pos-padding-cell
+
+
+
+ +
+ {% capture code %}{% raw %} +
+
+
Column 1
+
Column 2
+
Column 3
+
+
+
    +
  • + Column 1 + Content 1 +
  • +
  • + Column 1 + Content 2 +
  • +
  • + Column 3 + 321 +
  • +
+
+
+ {% endraw %}{% endcapture %} +
+
+
{{ code | lstrip | rstrip }}
+
+
+
+
+
+
+
Column 1
+
Column 2
+
Column 3
+
+
+
    +
  • + Column 1 + Content 1 +
  • +
  • + Column 1 + Content 2 +
  • +
  • + Column 3 + 321 +
  • +
+
    +
  • + Column 1 + Content 2 +
  • +
  • + Column 2 + Content 2 +
  • +
  • + Column 3 + 123 +
  • +
+
+
+
+
+
class
pos-table
+
props
--pos-padding-cell
+
+
+
+
diff --git a/modules/common-styling/public/views/partials/style-guide/tags.liquid b/modules/common-styling/public/views/partials/style-guide/tags.liquid new file mode 100644 index 0000000..cd2bf7d --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/tags.liquid @@ -0,0 +1,58 @@ +
+ +

Tags and badges

+ +
+ +
+
Class
pos-tag
+
Modifiers
pos-tag-confirmation, pos-tag-warning, pos-tag-important, pos-tag-interactive
+
Properties
--pos-radius-tag
+
+ +
+
+
    +
  • Default
  • +
  • Confirmation
  • +
  • Warning
  • +
  • Important
  • +
  • Interactive
  • +
+
+ {% capture code %}{% raw %} + Confirmation + {% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+ +
+ +

Tags list

+ +
+ +
+
Class
pos-tags-list
+
Properties
--pos-gap-tag-tag
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
\ No newline at end of file diff --git a/modules/common-styling/public/views/partials/style-guide/text-styles.liquid b/modules/common-styling/public/views/partials/style-guide/text-styles.liquid new file mode 100644 index 0000000..938ebd4 --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/text-styles.liquid @@ -0,0 +1,72 @@ +
+

Text styles

+

Sidenote

+
+
+
Class
pos-supplementary
+
Font family
+
Color
+
Size
+
Weight
+
Line height
+
+
+ + The quick brown fox jumps over the lazy dog + +{% capture code %}{% raw %} +The quick brown fox jumps over the lazy dog +{% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+ +

Tip

+
+
+
Class
pos-tip
+
Font family
+
Color
+
Size
+
Weight
+
Line height
+
+
+ + {% render 'modules/common-styling/tip', content: 'The quick brown fox jumps over the lazy dog' %} + + {% capture code %}{% raw %} + {% render 'modules/common-styling/tip', content: 'The quick brown fox jumps over the lazy dog' %} + {% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+
+
+ + +
+ +

Long text

+ +
+

Lorem ipsum dolor

+

Phasellus ultricies porta dui ac dapibus. Donec ipsum mi, interdum id turpis vel, aliquam ullamcorper orci.

+

Donec accumsan dignissim ligula, vitae imperdiet velit varius a. Phasellus quis elementum nibh. Suspendisse suscipit nisl sit amet quam tincidunt, in fermentum est mattis. Vivamus volutpat sagittis mattis. Praesent eu dapibus enim, in dignissim eros.

+ +
Donec laoreet vitae
+

Cras consequat, ipsum id consectetur elementum, nisl nulla blandit neque, ut commodo neque nisl non sapien. Integer rhoncus nisl semper nulla iaculis fringilla. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.

+

Morbi venenatis condimentum dolor, sit amet consequat est blandit eu. Nam dapibus mollis cursus.

+
    +
  1. Maecenas elementum nisi dolor, id viverra orci pretium placerat. Duis a elit quis purus pharetra vehicula. Vestibulum eu venenatis nisi.
  2. +
  3. Ut ante ex, ultrices non commodo ut, dictum at nibh.
  4. +
+ +
+ +
\ No newline at end of file diff --git a/modules/common-styling/public/views/partials/style-guide/toasts.liquid b/modules/common-styling/public/views/partials/style-guide/toasts.liquid new file mode 100644 index 0000000..bc76b94 --- /dev/null +++ b/modules/common-styling/public/views/partials/style-guide/toasts.liquid @@ -0,0 +1,66 @@ +
+

Toasts

+ +
+
+

A standard platformOS way of showing toast notifications would be to store and get the messages in the session.

+

Adding the following code to your application `layout` file will initialize the module:

+ + {% capture code %}{% raw %} +{% liquid + function flash = 'modules/core/commands/session/get', key: 'sflash' + if context.location.pathname != flash.from or flash.force_clear + function _ = 'modules/core/commands/session/clear', key: 'sflash' + endif + render 'modules/common-styling/toasts', params: flash +%} + {% endraw %}{% endcapture %} + +
+
{{ code | lstrip | rstrip }}
+
+ +

Then, you can use the following JavaScript to show a message on page:

+ {% capture code %}{% raw %} + new pos.modules.toast('[severity]', '[message]'); + {% endraw %}{% endcapture %} +
+
{{ code | lstrip | rstrip }}
+
+ +
+
severity
how important the message is - error, success, info string
+
message
user-readable message for the toast notification string
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
diff --git a/modules/common-styling/template-values.json b/modules/common-styling/template-values.json index 5e38b0b..aef7cfd 100644 --- a/modules/common-styling/template-values.json +++ b/modules/common-styling/template-values.json @@ -2,6 +2,6 @@ "name": "platformOS common styling", "machine_name": "common-styling", "type": "module", - "version": "1.29.0", + "version": "1.32.0", "dependencies": {} } \ No newline at end of file diff --git a/modules/core/generators/crud/templates/views/pages/model/delete.liquid b/modules/core/generators/crud/templates/views/pages/model/delete.liquid index f8781f5..bb26a02 100644 --- a/modules/core/generators/crud/templates/views/pages/model/delete.liquid +++ b/modules/core/generators/crud/templates/views/pages/model/delete.liquid @@ -8,9 +8,9 @@ method: delete # platformos-check-disable ConvertIncludeToRender if object.valid - include 'modules/core/helpers/redirect_to', url: '/<%= modelNamePlural %>', notice: 'app.models.shared.deleted' + include 'modules/core/helpers/redirect_to', url: '/<%= modelNamePlural %>', notice: 'modules/core/common.deleted' else - include 'modules/core/helpers/redirect_to', url: '/<%= modelNamePlural %>', error: 'app.models.shared.delete_failed' + include 'modules/core/helpers/redirect_to', url: '/<%= modelNamePlural %>', error: 'modules/core/common.delete_failed' endif # platformos-check-enable ConvertIncludeToRender %} diff --git a/modules/core/public/lib/commands/events/create.liquid b/modules/core/public/lib/commands/events/create.liquid index 9c4655c..50d44ce 100644 --- a/modules/core/public/lib/commands/events/create.liquid +++ b/modules/core/public/lib/commands/events/create.liquid @@ -4,7 +4,8 @@ if event.valid function event = 'modules/core/commands/events/create/execute', object: event if event.valid - function _r = 'modules/core/commands/events/broadcast', object: event, deprecated_max_attempts: deprecated_max_attempts, deprecated_delay: deprecated_delay + assign source_name = 'modules/core/commands/events/create:' | append: type + background _job_id = 'modules/core/commands/events/broadcast', object: event, deprecated_max_attempts: deprecated_max_attempts, deprecated_delay: deprecated_delay, source_name: source_name, priority: 'high' else log event, type: 'ERROR: modules/core/commands/events invalid' endif diff --git a/modules/core/public/lib/commands/events/create/check.liquid b/modules/core/public/lib/commands/events/create/check.liquid index bcb1708..b4f2878 100644 --- a/modules/core/public/lib/commands/events/create/check.liquid +++ b/modules/core/public/lib/commands/events/create/check.liquid @@ -5,7 +5,15 @@ function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'date' assign name = 'events/' | append: object.type - graphql event_check_partial = 'modules/core/events/events_checks', name: name | fetch: "admin_liquid_partials" | fetch: "results" | first + graphql event_check_partials = 'modules/core/events/events_checks', name: name | fetch: "admin_liquid_partials" | fetch: "results" + for partial in event_check_partials + assign is_event_definition = partial.path | matches: '^(modules/[^/]+/events/[^/]++|events/[^/]+)$' + if is_event_definition + assign event_check_partial = partial + break + endif + endfor + if event_check_partial function event_result = event_check_partial.path, event: object if event_result.valid != true diff --git a/modules/core/public/lib/commands/events/publish.liquid b/modules/core/public/lib/commands/events/publish.liquid index 7657b3e..75be92b 100644 --- a/modules/core/public/lib/commands/events/publish.liquid +++ b/modules/core/public/lib/commands/events/publish.liquid @@ -15,9 +15,7 @@ return null endunless - assign source_name = 'modules/core/commands/events/create:' | append: type + function event = "modules/core/commands/events/create", type: type, object: object, deprecated_max_attempts: max_attempts, deprecated_delay: delay - background job_id = "modules/core/commands/events/create", source_name: source_name, priority: 'high', type: type, object: object, deprecated_max_attempts: max_attempts, deprecated_delay: delay - - return job_id + return event %} diff --git a/modules/core/public/lib/helpers/authenticity_token.liquid b/modules/core/public/lib/helpers/authenticity_token.liquid new file mode 100644 index 0000000..b435480 --- /dev/null +++ b/modules/core/public/lib/helpers/authenticity_token.liquid @@ -0,0 +1,5 @@ +{% assign token = token | default: authenticity_token | default: context.authenticity_token %} +{% unless token %} + Liquid Error AuthenticityTokenNotFound +{% endunless %} + diff --git a/modules/core/public/lib/helpers/redirect_to.liquid b/modules/core/public/lib/helpers/redirect_to.liquid index b9025b5..3d430eb 100644 --- a/modules/core/public/lib/helpers/redirect_to.liquid +++ b/modules/core/public/lib/helpers/redirect_to.liquid @@ -1,21 +1,23 @@ {% liquid if url == blank and context.session.return_to != blank assign url = context.session.return_to + session return_to = null endif - if context.params.return_to - assign url = context.params.return_to | url_decode - endif - - assign url = url | default: '/' + if context.params.return_to != blank or context.params.redirect_to != blank and url == blank + assign url = context.params.return_to | default: context.params.redirect_to | url_decode + assign not_start_with_slash = url | matches: '^(?!\/)(.+)' - assign not_start_with_slash = url | matches: '^(?!\/)(.+)' - assign wrong_url = url | matches: '^\/\/' - if not_start_with_slash or wrong_url - assign url = '/' + # for security reasons, we do not allow redirecting to external URLs based on unsafe user input + assign wrong_url = url | matches: '^\/\/' + if not_start_with_slash or wrong_url + assign url = '/' + endif + else + assign default = default | default: '/' + assign url = url | default: default endif - # platformos-check-disable ConvertIncludeToRender include 'modules/core/helpers/flash/publish', notice: notice, error: error, info: info # platformos-check-enable ConvertIncludeToRender @@ -38,5 +40,3 @@ break %} - - diff --git a/modules/core/public/lib/helpers/timezone/get_all.liquid b/modules/core/public/lib/helpers/timezone/get_all.liquid index f16a4f8..7ed01d5 100644 --- a/modules/core/public/lib/helpers/timezone/get_all.liquid +++ b/modules/core/public/lib/helpers/timezone/get_all.liquid @@ -15,4 +15,4 @@ Returns an array of timezone objects in the following format: "friendly_name_without_region":"GMT+12" } {% endcomment %} -{% return context.globals.time_zones.all | to_json | parse_json %} +{% return context.globals.time_zones.all | parse_json %} diff --git a/modules/core/public/lib/helpers/timezone/get_by_offset.liquid b/modules/core/public/lib/helpers/timezone/get_by_offset.liquid new file mode 100644 index 0000000..f10f26e --- /dev/null +++ b/modules/core/public/lib/helpers/timezone/get_by_offset.liquid @@ -0,0 +1,21 @@ +{% comment %} + params: + - offset: string, e.g. "+2:00" + +returns a timezone object in the following format: +{ + "formatted_name":"(GMT-12:00) International Date Line West", + "formatted_offset":"-12:00", + "name":"International Date Line West", + "utc_offset":-43200, + "abbreviation":"-12", + "friendly_name_with_region":"Etc - GMT+12", + "friendly_name_without_region":"GMT+12" +} +{% endcomment %} +{% liquid + function timezones = 'modules/core/helpers/timezone/get_all' + assign timezone = timezones | array_detect: formatted_offset: offset + + return timezone +%} diff --git a/modules/core/public/lib/queries/constants/find.liquid b/modules/core/public/lib/queries/constants/find.liquid new file mode 100644 index 0000000..6d7c1dd --- /dev/null +++ b/modules/core/public/lib/queries/constants/find.liquid @@ -0,0 +1,34 @@ +{% if context.constants %} + {% assign value = context.constants[name] %} +{% else %} + {% graphql r, name: name %} + query get_constant($name: String!) { + constant(filter: { name: $name }) { + name + value + } + } + {% endgraphql %} + {% assign value = r.constant.value %} +{% endif %} + +{% liquid + case type + when "boolean" + if value == "true" + return true + else + return false + endif + when "integer" + assign value = value | plus: 0 + return value + when "array" + assign value = value | split: ',' + return value + when "time" + return value | to_time + else + return value + endcase +%} diff --git a/modules/core/public/lib/validations/is_url.liquid b/modules/core/public/lib/validations/is_url.liquid new file mode 100644 index 0000000..89bdaff --- /dev/null +++ b/modules/core/public/lib/validations/is_url.liquid @@ -0,0 +1,16 @@ +{% comment %} + params: @url + @field_name + @key[optional] +{% endcomment %} + +{% liquid + assign key = key | default: 'modules/core/validation.not_url' + assign is_url = url | matches: '^https?:\/\/[\S]+' + + if is_url != true + function c = 'modules/core/helpers/register_error', contract: c, field_name: field_name, key: key + endif + + return c +%} \ No newline at end of file diff --git a/modules/core/public/lib/validations/length.liquid b/modules/core/public/lib/validations/length.liquid index 34bb5cd..3be800e 100644 --- a/modules/core/public/lib/validations/length.liquid +++ b/modules/core/public/lib/validations/length.liquid @@ -20,10 +20,7 @@ assign allow_blank = true endif if allow_blank != true - if size == blank - assign message = message_blank | default: 'modules/core/validation.length.blank' | t - function c = 'modules/core/helpers/register_error', contract: c, field_name: field_name, message: message - endif + function c = 'modules/core/validations/presence', c: c, object: object, field_name: field_name endif if minimum != null and size < minimum diff --git a/modules/core/public/lib/validations/password_complexity.liquid b/modules/core/public/lib/validations/password_complexity.liquid index d483332..ad4b34d 100644 --- a/modules/core/public/lib/validations/password_complexity.liquid +++ b/modules/core/public/lib/validations/password_complexity.liquid @@ -9,7 +9,7 @@ assign decoded_pw = object.password assign minimum = minimum | default: 6 assign maximum = maximum | default: 256 - assign field_name = field_name | default: password + assign field_name = field_name | default: 'password' function complex_password = 'modules/core/queries/variable/find' name: "MODULES/CORE/USE_COMPLEX_PASSWORD", type: "boolean", context: context if complex_password diff --git a/modules/core/public/translations/en/common.yml b/modules/core/public/translations/en/common.yml new file mode 100644 index 0000000..19ed613 --- /dev/null +++ b/modules/core/public/translations/en/common.yml @@ -0,0 +1,4 @@ +en: + common: + deleted: 'Deleted' + deleted_failed: 'Deleted failed' diff --git a/modules/core/template-values.json b/modules/core/template-values.json index d18a732..87ab94b 100644 --- a/modules/core/template-values.json +++ b/modules/core/template-values.json @@ -2,6 +2,6 @@ "name": "Pos Module Core", "machine_name": "core", "type": "module", - "version": "1.5.2", + "version": "2.0.7", "dependencies": {} } diff --git a/modules/user/package-lock.json b/modules/user/package-lock.json new file mode 100644 index 0000000..378a87d --- /dev/null +++ b/modules/user/package-lock.json @@ -0,0 +1,318 @@ +{ + "name": "user", + "version": "1.0.2", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "user", + "version": "1.0.2", + "license": "MIT", + "devDependencies": { + "auto-changelog": "^2.4.0" + } + }, + "node_modules/auto-changelog": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.4.0.tgz", + "integrity": "sha512-vh17hko1c0ItsEcw6m7qPRf3m45u+XK5QyCrrBFViElZ8jnKrPC1roSznrd1fIB/0vR/zawdECCRJtTuqIXaJw==", + "dev": true, + "dependencies": { + "commander": "^7.2.0", + "handlebars": "^4.7.7", + "node-fetch": "^2.6.1", + "parse-github-url": "^1.0.2", + "semver": "^7.3.5" + }, + "bin": { + "auto-changelog": "src/index.js" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/parse-github-url": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", + "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==", + "dev": true, + "bin": { + "parse-github-url": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/uglify-js": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.2.tgz", + "integrity": "sha512-bbxglRjsGQMchfvXZNusUcYgiB9Hx2K4AHYXQy2DITZ9Rd+JzhX7+hoocE5Winr7z2oHvPsekkBwXtigvxevXg==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + }, + "dependencies": { + "auto-changelog": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.4.0.tgz", + "integrity": "sha512-vh17hko1c0ItsEcw6m7qPRf3m45u+XK5QyCrrBFViElZ8jnKrPC1roSznrd1fIB/0vR/zawdECCRJtTuqIXaJw==", + "dev": true, + "requires": { + "commander": "^7.2.0", + "handlebars": "^4.7.7", + "node-fetch": "^2.6.1", + "parse-github-url": "^1.0.2", + "semver": "^7.3.5" + } + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "parse-github-url": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", + "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==", + "dev": true + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "uglify-js": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.2.tgz", + "integrity": "sha512-bbxglRjsGQMchfvXZNusUcYgiB9Hx2K4AHYXQy2DITZ9Rd+JzhX7+hoocE5Winr7z2oHvPsekkBwXtigvxevXg==", + "dev": true, + "optional": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } +} diff --git a/modules/user/package.json b/modules/user/package.json new file mode 100644 index 0000000..720b25a --- /dev/null +++ b/modules/user/package.json @@ -0,0 +1,26 @@ +{ + "name": "user", + "version": "1.0.4", + "description": "This module handles the user operations, assign users to roles and add permissions to roles.", + "scripts": { + "version": "(cd ../../ && pos-cli modules version user -p) && git add template-values.json && auto-changelog -p && git add CHANGELOG.md" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Platform-OS/pos-module-user.git" + }, + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/Platform-OS/pos-module-user/issues" + }, + "homepage": "https://github.com/Platform-OS/pos-module-user#readme", + "devDependencies": { + "auto-changelog": "^2.4.0" + }, + "auto-changelog": { + "template": "changelog-template.hbs", + "unreleased": true, + "commitLimit": false + } +} diff --git a/modules/user/public/assets/style/pos-user-form.css b/modules/user/public/assets/style/pos-user-form.css new file mode 100644 index 0000000..ec8608d --- /dev/null +++ b/modules/user/public/assets/style/pos-user-form.css @@ -0,0 +1,116 @@ +/* + styling for the user forms + + layout + texts + form +*/ + + + +/* layout +============================================================================ */ +.pos-user-content { + width: 100%; + max-width: 350px; +} + + + +/* texts +============================================================================ */ +.pos-user-content .pos-heading-2 { + margin-block-end: calc(var(--pos-gap-text-text) / 3); +} + + + +/* form +============================================================================ */ +.pos-user-content .pos-form { + margin-block-start: var(--pos-gap-title-content); + margin-block-end: var(--pos-gap-content-footer); +} + +.pos-user-content fieldset { + display: flex; + flex-direction: column; + gap: .2em; +} + +.pos-user-label-password { + display: flex; + justify-content: space-between; +} + +.pos-user-label-password a:not(:hover):not(:active) { + color: var(--pos-color-content-foreground-supplementary); +} + +.pos-user-social-login-separator { + margin: 20px 0px; + text-align: center; + display: flex; + flex-wrap: nowrap; + align-items: center; + gap: 8px; + text-transform: uppercase; +} + +.pos-user-social-login-separator::before, +.pos-user-social-login-separator::after { + content: ""; + display: inline-block; + border-top: 1px solid var(--pos-color-content-foreground-supplementary); + flex-grow: 1; +} + +.pos-user-content .pos-user-social-login-providers form { + margin-top: 0; + margin-bottom: 10px; +} + +.pos-user-content .pos-user-social-login-providers .pos-button { + width: 100%; +} + +.pos-button-social { + display: flex; + place-items: center; +} + +.pos-button-social { + width: 100%; + box-sizing: border-box; +} + +.pos-button-social-icon { + width: 30%; + display: flex; + place-content: center; + place-items: center; +} + +.pos-button-social-text { + width: 70%; +} + +.pos-social-listing { + margin: 10px 0; + display: flex; + flex-direction: column; + max-width: 350px; + gap: 10px; +} + +.pos-2fa-input { + width: 50px; +} + +.pos-2fa-spacing { + padding-top: var(--pos-gap-title-content); +} + +.pos-user-2fa .submit-2fa { + margin-top: 5px; +} \ No newline at end of file diff --git a/modules/user/public/graphql/api_call.graphql b/modules/user/public/graphql/api_call.graphql new file mode 100644 index 0000000..2aab5d7 --- /dev/null +++ b/modules/user/public/graphql/api_call.graphql @@ -0,0 +1,19 @@ +mutation api_call( + $api_template: String! + $data: HashObject! + $timeout: Int = 60 +) { + api_call_send( + data: $data + template: { name: $api_template } + options: { timeout: $timeout } + ) { + response { + status + body + } + errors { + message + } + } +} diff --git a/modules/user/public/graphql/oauth/create.graphql b/modules/user/public/graphql/oauth/create.graphql new file mode 100644 index 0000000..26fc424 --- /dev/null +++ b/modules/user/public/graphql/oauth/create.graphql @@ -0,0 +1,21 @@ +mutation ($user_id: String!, $provider: String!, $sub: String!) { + record: record_create( + record: { + table: "modules/user/oauth" + properties: [ + { name: "user_id", value: $user_id } + { name: "provider", value: $provider } + { name: "sub", value: $sub } + ] + } + ) { + id + create_at: created_at + updated_at: updated_at + table + + user_id: property(name: "user_id") + provider: property(name: "provider") + sub: property(name: "sub") + } +} diff --git a/modules/user/public/graphql/oauth/delete.graphql b/modules/user/public/graphql/oauth/delete.graphql new file mode 100644 index 0000000..8fea3d6 --- /dev/null +++ b/modules/user/public/graphql/oauth/delete.graphql @@ -0,0 +1,6 @@ +mutation delete($id: ID!) { + record: record_delete( + table: "modules/user/oauth" + id: $id + ){ id } +} diff --git a/modules/user/public/graphql/oauth/find_by_sub.graphql b/modules/user/public/graphql/oauth/find_by_sub.graphql new file mode 100644 index 0000000..86c78ca --- /dev/null +++ b/modules/user/public/graphql/oauth/find_by_sub.graphql @@ -0,0 +1,30 @@ +query find_by_sub( + $sub: String! + $provider: String! +) { + records( + per_page: 1 + filter: { + table: { value: "modules/user/oauth" } + properties: [ + { name: "sub", value: $sub } + { name: "provider", value: $provider } + ] + } + ) { + total_entries + total_pages + has_previous_page + has_next_page + results { + id + created_at: created_at + updated_at: updated_at + table + + user_id: property(name: "user_id") + sub: property(name: "sub") + provider: property(name: "provider") + } + } +} diff --git a/modules/user/public/graphql/oauth/find_by_user_id.graphql b/modules/user/public/graphql/oauth/find_by_user_id.graphql new file mode 100644 index 0000000..9ffa0d2 --- /dev/null +++ b/modules/user/public/graphql/oauth/find_by_user_id.graphql @@ -0,0 +1,29 @@ +query find_by_user_id( + $user_id: String! + $provider: String = null +) { + records( + per_page: 1 + filter: { + table: { value: "modules/user/oauth" } + properties: [ + { name: "user_id", value: $user_id } + { name: "provider", value: $provider } + ] + } + ) { + total_entries + total_pages + has_previous_page + has_next_page + results { + id + created_at: created_at + updated_at: updated_at + table + + user_id: property(name: "user_id") + provider: property(name: "provider") + } + } +} diff --git a/modules/user/public/graphql/profiles/create.graphql b/modules/user/public/graphql/profiles/create.graphql new file mode 100644 index 0000000..a3e1d5f --- /dev/null +++ b/modules/user/public/graphql/profiles/create.graphql @@ -0,0 +1,39 @@ +mutation ( + $uuid: String! + $user_id: String! + $first_name: String + $last_name: String + $name: String + $email: String + $roles: [String] + $c__names: String +) { + record: record_create( + record: { + table: "modules/user/profile" + properties: [ + { name: "user_id", value: $user_id } + { name: "first_name", value: $first_name } + { name: "last_name", value: $last_name } + { name: "uuid", value: $uuid } + { name: "name", value: $name } + { name: "email", value: $email } + { name: "roles", value_array: $roles } + { name: "c__names", value: $c__names } + ] + } + ) { + id + created_at + type: table + + email: property(name: "email") + first_name: property(name: "first_name") + last_name: property(name: "last_name") + name: property(name: "name") + user_id: property(name: "user_id") + uuid: property(name: "uuid") + roles: property_array(name: "roles") + otp_configured: property_boolean(name: "otp_configured") + } +} diff --git a/modules/user/public/graphql/profiles/delete.graphql b/modules/user/public/graphql/profiles/delete.graphql new file mode 100644 index 0000000..8ceda25 --- /dev/null +++ b/modules/user/public/graphql/profiles/delete.graphql @@ -0,0 +1,6 @@ +mutation delete_profile($id: ID!) { + record: record_delete( + table: "modules/user/profile" + id: $id + ){ id } +} diff --git a/modules/user/public/graphql/profiles/mark_otp.graphql b/modules/user/public/graphql/profiles/mark_otp.graphql new file mode 100644 index 0000000..5d023c1 --- /dev/null +++ b/modules/user/public/graphql/profiles/mark_otp.graphql @@ -0,0 +1,17 @@ +mutation mark_otp( + $id: ID! + $otp_configured: Boolean! +) { + record: record_update( + id: $id + record: { + table: "modules/user/profile" + properties: [ + { name: "otp_configured", value_boolean: $otp_configured } + ] + } + ) { + id + otp_configured: property_boolean(name: "otp_configured") + } +} diff --git a/modules/user/public/graphql/profiles/roles/append.graphql b/modules/user/public/graphql/profiles/roles/append.graphql new file mode 100644 index 0000000..27d801f --- /dev/null +++ b/modules/user/public/graphql/profiles/roles/append.graphql @@ -0,0 +1,26 @@ +mutation ( + $id: ID! + $role: String! +) { + record: record_update( + id: $id + record: { + table: "modules/user/profile" + properties: [ + { name: "roles", array_append: $role } + ] + } + ) { + id + created_at + type: table + + email: property(name: "email") + first_name: property(name: "first_name") + last_name: property(name: "last_name") + name: property(name: "name") + user_id: property(name: "user_id") + uuid: property(name: "uuid") + roles: property_array(name: "roles") + } +} diff --git a/modules/user/public/graphql/profiles/roles/remove.graphql b/modules/user/public/graphql/profiles/roles/remove.graphql new file mode 100644 index 0000000..37bda28 --- /dev/null +++ b/modules/user/public/graphql/profiles/roles/remove.graphql @@ -0,0 +1,26 @@ +mutation ( + $id: ID! + $role: String! +) { + record: record_update( + id: $id + record: { + table: "modules/user/profile" + properties: [ + { name: "roles", array_remove: $role } + ] + } + ) { + id + created_at + type: table + + email: property(name: "email") + first_name: property(name: "first_name") + last_name: property(name: "last_name") + name: property(name: "name") + user_id: property(name: "user_id") + uuid: property(name: "uuid") + roles: property_array(name: "roles") + } +} diff --git a/modules/user/public/graphql/profiles/roles/set.graphql b/modules/user/public/graphql/profiles/roles/set.graphql new file mode 100644 index 0000000..0f341ee --- /dev/null +++ b/modules/user/public/graphql/profiles/roles/set.graphql @@ -0,0 +1,26 @@ +mutation ( + $id: ID! + $roles: [String!] +) { + record: record_update( + id: $id + record: { + table: "modules/user/profile" + properties: [ + { name: "roles", value_array: $roles } + ] + } + ) { + id + created_at + type: table + + email: property(name: "email") + first_name: property(name: "first_name") + last_name: property(name: "last_name") + name: property(name: "name") + user_id: property(name: "user_id") + uuid: property(name: "uuid") + roles: property_array(name: "roles") + } +} diff --git a/modules/user/public/graphql/profiles/search.graphql b/modules/user/public/graphql/profiles/search.graphql new file mode 100644 index 0000000..6dccfc8 --- /dev/null +++ b/modules/user/public/graphql/profiles/search.graphql @@ -0,0 +1,52 @@ +query ( + $page: Int = 1 + $limit: Int = 20 + $first_name: String + $last_name: String + $email: String + $emails: [String!] + $id: ID + $ids: [ID!] + $not_ids: [ID!] + $uuid: String + $user_id: String + $sort: RecordsSortInput = { created_at: { order: DESC } } + $query: String +) { + records( + page: $page + per_page: $limit + filter: { + id: { value: $id, value_in: $ids, not_value_in: $not_ids } + table: { value: "modules/user/profile" } + properties: [ + { name: "uuid", value: $uuid } + { name: "first_name", value: $first_name } + { name: "last_name", value: $last_name } + { name: "user_id", value: $user_id } + { name: "email", value: $email, value_in: $emails } + { name: "c__names", contains: $query } + ] + } + sort: [$sort] + ) { + total_entries + total_pages + has_previous_page + has_next_page + results { + id + created_at + type: table + + email: property(name: "email") + first_name: property(name: "first_name") + last_name: property(name: "last_name") + name: property(name: "name") + user_id: property(name: "user_id") + uuid: property(name: "uuid") + roles: property_array(name: "roles") + otp_configured: property_boolean(name: "otp_configured") + } + } +} diff --git a/modules/user/public/graphql/profiles/update.graphql b/modules/user/public/graphql/profiles/update.graphql new file mode 100644 index 0000000..b47b790 --- /dev/null +++ b/modules/user/public/graphql/profiles/update.graphql @@ -0,0 +1,37 @@ +mutation profiles_update( + $id: ID! + $name: String + $first_name: String + $last_name: String + $email: String + $roles: [String] + $c__names: String +) { + record: record_update( + id: $id + record: { + table: "modules/user/profile" + properties: [ + { name: "name", value: $name } + { name: "first_name", value: $first_name } + { name: "last_name", value: $last_name } + { name: "email", value: $email } + { name: "roles", value_array: $roles } + { name: "c__names", value: $c__names } + ] + } + ) { + id + created_at + type: table + + email: property(name: "email") + first_name: property(name: "first_name") + last_name: property(name: "last_name") + name: property(name: "name") + user_id: property(name: "user_id") + uuid: property(name: "uuid") + roles: property_array(name: "roles") + otp_configured: property_boolean(name: "otp_configured") + } +} diff --git a/modules/user/public/graphql/session/destroy.graphql b/modules/user/public/graphql/session/destroy.graphql new file mode 100644 index 0000000..a268679 --- /dev/null +++ b/modules/user/public/graphql/session/destroy.graphql @@ -0,0 +1,3 @@ +mutation { + user_session_destroy +} diff --git a/modules/user/public/graphql/user/count.graphql b/modules/user/public/graphql/user/count.graphql new file mode 100644 index 0000000..aa197dd --- /dev/null +++ b/modules/user/public/graphql/user/count.graphql @@ -0,0 +1,5 @@ +query { + users(per_page: 1) { + total_entries + } +} diff --git a/modules/user/public/graphql/user/create.graphql b/modules/user/public/graphql/user/create.graphql new file mode 100644 index 0000000..8acc3ee --- /dev/null +++ b/modules/user/public/graphql/user/create.graphql @@ -0,0 +1,6 @@ +mutation ($email: String!, $password: String!) { + user: user_create(user: { email: $email, password: $password, properties: []}) { + id + email + } +} diff --git a/modules/user/public/graphql/user/delete.graphql b/modules/user/public/graphql/user/delete.graphql new file mode 100644 index 0000000..639c772 --- /dev/null +++ b/modules/user/public/graphql/user/delete.graphql @@ -0,0 +1,8 @@ +mutation ($id: ID!) { + user: user_delete(id: $id) { + created_at + deleted_at + id + email + } +} diff --git a/modules/user/public/graphql/user/email_update.graphql b/modules/user/public/graphql/user/email_update.graphql new file mode 100644 index 0000000..600a568 --- /dev/null +++ b/modules/user/public/graphql/user/email_update.graphql @@ -0,0 +1,10 @@ +mutation update_email($id: ID!, $email: String!) { + user: user_update( + id: $id + user: { + email: $email + } + ){ + id + } +} diff --git a/modules/user/public/graphql/user/emails_count.graphql b/modules/user/public/graphql/user/emails_count.graphql new file mode 100644 index 0000000..e1659a1 --- /dev/null +++ b/modules/user/public/graphql/user/emails_count.graphql @@ -0,0 +1,5 @@ +query emails_count($email: String!){ + users(filter: { email: { value: $email} }, per_page:1){ + total_entries + } +} diff --git a/modules/user/public/graphql/user/find.graphql b/modules/user/public/graphql/user/find.graphql new file mode 100644 index 0000000..9887224 --- /dev/null +++ b/modules/user/public/graphql/user/find.graphql @@ -0,0 +1,23 @@ +query ( + $id: ID + $email: String + $with_token: Boolean = false + $valid_for: Int = 1 + $expires_in: Float = 48 + $limit: Int = 1 +) { + users( + per_page: $limit, + filter: { + id: { value: $id } + email: { value: $email } + } + ) { + results { + created_at + email + id + token: temporary_token(valid_for: $valid_for, expires_in: $expires_in) @include(if: $with_token) + } + } +} diff --git a/modules/user/public/graphql/user/list.graphql b/modules/user/public/graphql/user/list.graphql new file mode 100644 index 0000000..79c111e --- /dev/null +++ b/modules/user/public/graphql/user/list.graphql @@ -0,0 +1,10 @@ +query users_list($email:String) { + users(per_page: 1000, filter: { email: { exact: $email } }) { + total_entries + results { + created_at + email + id + } + } +} diff --git a/modules/user/public/graphql/user/load.graphql b/modules/user/public/graphql/user/load.graphql new file mode 100644 index 0000000..8cd98ef --- /dev/null +++ b/modules/user/public/graphql/user/load.graphql @@ -0,0 +1,10 @@ +query ($id: ID!) { + users(per_page: 1, filter: { id: { value: $id } }) { + results { + created_at + email + id, + roles: property_array(name: "roles") + } + } +} diff --git a/modules/user/public/graphql/user/otp.graphql b/modules/user/public/graphql/user/otp.graphql new file mode 100644 index 0000000..d598ca7 --- /dev/null +++ b/modules/user/public/graphql/user/otp.graphql @@ -0,0 +1,15 @@ +query otp($email: String!, $issuer: String!){ + users( + filter: { email: { value: $email} }, per_page:1 + ){ + results{ + id + email + otp { + current_code + secret_as_svg_qr_code(label: $email, issuer: $issuer) + secret + } + } + } +} diff --git a/modules/user/public/graphql/user/search.graphql b/modules/user/public/graphql/user/search.graphql new file mode 100644 index 0000000..55bc396 --- /dev/null +++ b/modules/user/public/graphql/user/search.graphql @@ -0,0 +1,35 @@ +query ( + $id: ID + $not_ids: [ID!] + $email: String + $limit: Int = 20 + $page: Int = 1 + $sort: UsersSortInput = { id: { order: DESC } } + $include_profiles: Boolean = false +) { + users( + per_page: $limit + page: $page + filter: { + id: { value: $id, not_value_in: $not_ids } + email: { value: $email } + } + sort: [$sort] + ) { + results { + id + email + created_at + roles: property_array(name: "roles") + profiles: related_records( + table: "modules/user/profile" + join_on_property: "id" + foreign_property: "user_id" + limit: 1 + ) @include(if: $include_profiles) { + id + properties + } + } + } +} diff --git a/modules/user/public/graphql/user/update.graphql b/modules/user/public/graphql/user/update.graphql new file mode 100644 index 0000000..00ea415 --- /dev/null +++ b/modules/user/public/graphql/user/update.graphql @@ -0,0 +1,6 @@ +mutation ($id: ID!, $email: String, $password: String) { + user: user_update(id: $id, user: { email: $email, password: $password, properties: [] }) { + id + email + } +} diff --git a/modules/user/public/graphql/user/update_password.graphql b/modules/user/public/graphql/user/update_password.graphql new file mode 100644 index 0000000..5951eab --- /dev/null +++ b/modules/user/public/graphql/user/update_password.graphql @@ -0,0 +1,10 @@ +mutation update_password($id: ID!, $password: String) { + user: user_update( + id: $id + user: { + password: $password + } + ){ + id + } +} diff --git a/modules/user/public/graphql/user/verify_otp.graphql b/modules/user/public/graphql/user/verify_otp.graphql new file mode 100644 index 0000000..556d415 --- /dev/null +++ b/modules/user/public/graphql/user/verify_otp.graphql @@ -0,0 +1,14 @@ +query verify($email: String!, $otp_code: String!, $password: String!){ + users( + filter: { email: { value: $email} }, per_page:1 + ){ + results{ + id + email + authenticate{ + otp_code(code: $otp_code, drift: 30) + password(password: $password) + } + } + } +} diff --git a/modules/user/public/graphql/user/verify_password.graphql b/modules/user/public/graphql/user/verify_password.graphql new file mode 100644 index 0000000..37a543a --- /dev/null +++ b/modules/user/public/graphql/user/verify_password.graphql @@ -0,0 +1,11 @@ +query ($email: String!, $password: String!) { + users(filter: { email: { value: $email } }, per_page: 1) { + results { + id + email + authenticate { + password(password: $password) + } + } + } +} diff --git a/modules/user/public/graphql/user/verify_password_for_user_id.graphql b/modules/user/public/graphql/user/verify_password_for_user_id.graphql new file mode 100644 index 0000000..98b78f0 --- /dev/null +++ b/modules/user/public/graphql/user/verify_password_for_user_id.graphql @@ -0,0 +1,16 @@ +query verify($id: ID!, $password: String!){ + users( + filter: { + id: { value: $id } + } + per_page: 1 + ){ + results{ + id + email + authenticate{ + password(password: $password) + } + } + } +} diff --git a/modules/user/public/lib/commands/authentication_links/create.liquid b/modules/user/public/lib/commands/authentication_links/create.liquid new file mode 100644 index 0000000..3babc6a --- /dev/null +++ b/modules/user/public/lib/commands/authentication_links/create.liquid @@ -0,0 +1,12 @@ +{% liquid + function object = 'modules/user/commands/authentication_links/create/build', email: email, host: host, valid_for: valid_for + function object = 'modules/user/commands/authentication_links/create/check', object: object, hcaptcha_params: hcaptcha_params + + if object.valid + function object = 'modules/user/commands/authentication_links/create/execute', object: object + assign event_payload = null | hash_merge: email: email + function _ = 'modules/core/commands/events/publish', type: 'authentication_link_created', object: event_payload, delay: null, max_attempts: null + endif + + return object +%} diff --git a/modules/user/public/lib/commands/authentication_links/create/build.liquid b/modules/user/public/lib/commands/authentication_links/create/build.liquid new file mode 100644 index 0000000..435aaa7 --- /dev/null +++ b/modules/user/public/lib/commands/authentication_links/create/build.liquid @@ -0,0 +1,17 @@ +{% liquid + assign valid_for = valid_for | default: 5 + function user = 'modules/user/queries/user/find', email: email, with_token: true +%} + +{% parse_json object %} +{ + "email": "{{ email }}", + "id": "{{ user.id }}", + "token": "{{ user.token }}", + "host": "{{ host }}" +} +{% endparse_json %} + +{% liquid + return object +%} diff --git a/modules/user/public/lib/commands/authentication_links/create/check.liquid b/modules/user/public/lib/commands/authentication_links/create/check.liquid new file mode 100644 index 0000000..cdd52a2 --- /dev/null +++ b/modules/user/public/lib/commands/authentication_links/create/check.liquid @@ -0,0 +1,16 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'email' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'host' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'token' + + if context.constants.VERIFY_HCAPTCHA == "true" + function c = 'modules/core/validations/hcaptcha', c: c, hcaptcha_params: hcaptcha_params + endif + + hash_assign object['valid'] = c.valid + hash_assign object['errors'] = c.errors + + return object +%} diff --git a/modules/user/public/lib/commands/authentication_links/create/execute.liquid b/modules/user/public/lib/commands/authentication_links/create/execute.liquid new file mode 100644 index 0000000..2b5ea1e --- /dev/null +++ b/modules/user/public/lib/commands/authentication_links/create/execute.liquid @@ -0,0 +1,7 @@ +{% liquid + if object.valid + hash_assign object['url'] = 'https://{host}/passwords/new?token={token}&email={email}' | expand_url_template: object + endif + + return object +%} diff --git a/modules/user/public/lib/commands/emails/auth-link.liquid b/modules/user/public/lib/commands/emails/auth-link.liquid new file mode 100644 index 0000000..5ce5df4 --- /dev/null +++ b/modules/user/public/lib/commands/emails/auth-link.liquid @@ -0,0 +1,28 @@ +--- +metadata: + parameters: + object: + - url + - email + - id +--- +{% parse_json object %} + { + "to": {{ object.email | json }}, + "from": {% print 'modules/user/emails.from_email' | t: default: 'noreply@platformos.com' | json %}, + "subject": {% print 'modules/user/emails.passwords.reset.subject' | t | json %}, + "partial": "modules/user/emails/passwords/reset", + "layout": null, + "data": { + "url": {% print object.url | json %}, + "user": { + "id": {% print object.id | json %} + } + } + } +{% endparse_json %} + +{% liquid + function object = 'modules/core/commands/email/send', object: object + return object +%} diff --git a/modules/user/public/lib/commands/oauth/create_user.liquid b/modules/user/public/lib/commands/oauth/create_user.liquid new file mode 100644 index 0000000..3f49ab1 --- /dev/null +++ b/modules/user/public/lib/commands/oauth/create_user.liquid @@ -0,0 +1,9 @@ +{% liquid + assign password = 30 | random_string + assign full_name = user_first_name | append: " " | append: user_last_name + assign object = "{}" | parse_json | hash_merge: email: user_email + assign object = object | hash_merge: firstName: user_first_name, lastName: user_last_name, fullName: full_name + + function new_user = "modules/user/commands/user/create", first_name: user_first_name, last_name: user_last_name, email: user_email, password: password, hook_params: object, roles: null + return new_user +%} \ No newline at end of file diff --git a/modules/user/public/lib/commands/passwords/create.liquid b/modules/user/public/lib/commands/passwords/create.liquid new file mode 100644 index 0000000..fb11f56 --- /dev/null +++ b/modules/user/public/lib/commands/passwords/create.liquid @@ -0,0 +1,12 @@ +{% liquid + function object = 'modules/user/commands/passwords/create/build', object: object + function object = 'modules/user/commands/passwords/create/check', object: object + + if object.valid + function object = 'modules/user/commands/passwords/create/execute', object: object + assign event_payload = null | hash_merge: user_id: object.user_id + function _ = 'modules/core/commands/events/publish', type: 'password_created', object: event_payload, delay: null, max_attempts: null + endif + + return object +%} diff --git a/modules/user/public/lib/commands/passwords/create/build.liquid b/modules/user/public/lib/commands/passwords/create/build.liquid new file mode 100644 index 0000000..3fb14d0 --- /dev/null +++ b/modules/user/public/lib/commands/passwords/create/build.liquid @@ -0,0 +1,16 @@ +--- +metadata: + parameters: + - name: password + - name: password_confirmation + - name: user_id +--- +{% parse_json object %} +{ + "id": {{ object.user_id | json }}, + "password": {{ object.password | json }}, + "password_confirmation": {{ object.password_confirmation | json }} +} +{% endparse_json %} + +{% return object %} diff --git a/modules/user/public/lib/commands/passwords/create/check.liquid b/modules/user/public/lib/commands/passwords/create/check.liquid new file mode 100644 index 0000000..877fbe9 --- /dev/null +++ b/modules/user/public/lib/commands/passwords/create/check.liquid @@ -0,0 +1,14 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'id' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'password' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'password_confirmation' + function c = 'modules/core/validations/equal', c: c, given: object.password, expected: object.password_confirmation, field_name: 'password_confirmation', key: 'modules/user/validation.password.do_not_match', not_verbose: true, message: null + function c = 'modules/core/validations/password_complexity', c: c, object: object, field_name: 'password', key: null + + hash_assign object['valid'] = c.valid + hash_assign object['errors'] = c.errors + + return object +%} diff --git a/modules/user/public/lib/commands/passwords/create/execute.liquid b/modules/user/public/lib/commands/passwords/create/execute.liquid new file mode 100644 index 0000000..4c0a7fd --- /dev/null +++ b/modules/user/public/lib/commands/passwords/create/execute.liquid @@ -0,0 +1,12 @@ +{% liquid + graphql r = 'modules/user/user/update_password', args: object + + if r.errors + log r.errors, type: 'errors.graphql.invalid' + + hash_assign object['valid'] = false + hash_assign object['errors'] = r.errors + endif + + return object +%} diff --git a/modules/user/public/lib/commands/profiles/create.liquid b/modules/user/public/lib/commands/profiles/create.liquid new file mode 100644 index 0000000..d5557ae --- /dev/null +++ b/modules/user/public/lib/commands/profiles/create.liquid @@ -0,0 +1,9 @@ +{% liquid + function object = 'modules/user/commands/profiles/create/build', object: object, name: null + function object = 'modules/user/commands/profiles/create/check', object: object + if object.valid + function object = 'modules/core/commands/execute', mutation_name: 'modules/user/profiles/create', object: object + assign object = object | hash_merge: object.properties + endif + return object +%} diff --git a/modules/user/public/lib/commands/profiles/create/build.liquid b/modules/user/public/lib/commands/profiles/create/build.liquid new file mode 100644 index 0000000..ff6f895 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/create/build.liquid @@ -0,0 +1,32 @@ +{% liquid + function tokenized_names = 'modules/user/commands/profiles/tokenize_names', object: object + assign uuid_new = '' | uuid + assign uuid = object.uuid | default: uuid_new + assign name = object.first_name | append: ' ' | append: object.last_name + + assign data = null | hash_merge: first_name: object.first_name, last_name: object.last_name, user_id: object.user_id, email: object.email, uuid: uuid, name: name, c__names: tokenized_names + if object.roles == null + assign roles = '[]' | parse_json + if context.constants.USER_DEFAULT_ROLE != blank + assign roles = roles | array_add: context.constants.USER_DEFAULT_ROLE + endif + else + assign roles_type = object.roles | type_of + if roles_type == 'String' + assign roles = object.roles | split: ',' + elsif roles_type == 'Array' + assign roles = object.roles + else + # accepts only String and Array + log object.roles, type: 'ERROR: roles must be an array or a coma separated string' + assign roles = '[]' | parse_json + + if context.constants.USER_DEFAULT_ROLE != blank + assign roles = roles | array_add: context.constants.USER_DEFAULT_ROLE + endif + endif + endif + + hash_assign data['roles'] = roles + return data +%} diff --git a/modules/user/public/lib/commands/profiles/create/check.liquid b/modules/user/public/lib/commands/profiles/create/check.liquid new file mode 100644 index 0000000..2c9b645 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/create/check.liquid @@ -0,0 +1,14 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'user_id' + function c = 'modules/core/validations/uniqueness', c: c, object: object, field_name: 'user_id', table: 'modules/user/profile', scope_name: null, exclude_name: null + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'uuid' + function c = 'modules/core/validations/length', c: c, object: object, field_name: 'name', maximum: 80, allow_blank: null + function c = 'modules/core/validations/length', c: c, object: object, field_name: 'first_name', maximum: 40, allow_blank: null + function c = 'modules/core/validations/length', c: c, object: object, field_name: 'last_name', maximum: 40, allow_blank: null + + assign object = object | hash_merge: c + + return object +%} diff --git a/modules/user/public/lib/commands/profiles/create_proxy.liquid b/modules/user/public/lib/commands/profiles/create_proxy.liquid new file mode 100644 index 0000000..a2b9854 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/create_proxy.liquid @@ -0,0 +1,6 @@ +{% liquid + function profile_module = 'modules/core/queries/variable/get', name: 'PROFILE_MODULE', default: 'modules/user', type: null + assign function_name = profile_module | append: '/commands/profiles/create' + function profile = function_name, object: object + return profile +%} diff --git a/modules/user/public/lib/commands/profiles/create_validate.liquid b/modules/user/public/lib/commands/profiles/create_validate.liquid new file mode 100644 index 0000000..fe043db --- /dev/null +++ b/modules/user/public/lib/commands/profiles/create_validate.liquid @@ -0,0 +1,6 @@ +{% liquid + function object = 'modules/user/commands/profiles/create/build', object: object, name: null + function object = 'modules/user/commands/profiles/create/check', object: object + + return object +%} diff --git a/modules/user/public/lib/commands/profiles/create_validate_proxy.liquid b/modules/user/public/lib/commands/profiles/create_validate_proxy.liquid new file mode 100644 index 0000000..69d579f --- /dev/null +++ b/modules/user/public/lib/commands/profiles/create_validate_proxy.liquid @@ -0,0 +1,6 @@ +{% liquid + function profile_module = 'modules/core/queries/variable/get', name: 'PROFILE_MODULE', default: 'modules/user', type: null + assign function_name = profile_module | append: '/commands/profiles/create_validate' + function profile = function_name, object: object + return profile +%} diff --git a/modules/user/public/lib/commands/profiles/delete.liquid b/modules/user/public/lib/commands/profiles/delete.liquid new file mode 100644 index 0000000..0cfb315 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/delete.liquid @@ -0,0 +1,8 @@ +{% liquid + function object = 'modules/user/commands/profiles/delete/build', object: object + function object = 'modules/user/commands/profiles/delete/check', object: object + if object.valid + function object = 'modules/core/commands/execute', mutation_name: 'modules/user/profiles/delete', object: object + endif + return object +%} diff --git a/modules/user/public/lib/commands/profiles/delete/build.liquid b/modules/user/public/lib/commands/profiles/delete/build.liquid new file mode 100644 index 0000000..d10a19e --- /dev/null +++ b/modules/user/public/lib/commands/profiles/delete/build.liquid @@ -0,0 +1,5 @@ +{% liquid + assign data = null | hash_merge: id: object.id + + return data +%} diff --git a/modules/user/public/lib/commands/profiles/delete/check.liquid b/modules/user/public/lib/commands/profiles/delete/check.liquid new file mode 100644 index 0000000..67e2c81 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/delete/check.liquid @@ -0,0 +1,9 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'id' + + assign object = object | hash_merge: c + + return object +%} diff --git a/modules/user/public/lib/commands/profiles/mark_otp.liquid b/modules/user/public/lib/commands/profiles/mark_otp.liquid new file mode 100644 index 0000000..c54a67e --- /dev/null +++ b/modules/user/public/lib/commands/profiles/mark_otp.liquid @@ -0,0 +1,10 @@ +{% liquid + function object = 'modules/user/commands/profiles/mark_otp/build', object: object + function object = 'modules/user/commands/profiles/mark_otp/check', object: object + + if object.valid + function object = 'modules/core/commands/execute', mutation_name: 'modules/user/profiles/mark_otp' object: object + endif + + return object +%} diff --git a/modules/user/public/lib/commands/profiles/mark_otp/build.liquid b/modules/user/public/lib/commands/profiles/mark_otp/build.liquid new file mode 100644 index 0000000..87e9177 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/mark_otp/build.liquid @@ -0,0 +1,10 @@ +{% liquid + assign data = '{}' | parse_json | hash_merge: id: object.id + if object.otp_configured == null + hash_assign data['otp_configured'] = true + else + hash_assign data['otp_configured'] = object.otp_configured + endif + + return data +%} diff --git a/modules/user/public/lib/commands/profiles/mark_otp/check.liquid b/modules/user/public/lib/commands/profiles/mark_otp/check.liquid new file mode 100644 index 0000000..9c54ed4 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/mark_otp/check.liquid @@ -0,0 +1,11 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'id' + function c = 'modules/core/validations/not_null', c: c, object: object, field_name: 'otp_configured' + + hash_assign object['valid'] = c.valid + hash_assign object['errors'] = c.errors + + return object +%} diff --git a/modules/user/public/lib/commands/profiles/roles/append.liquid b/modules/user/public/lib/commands/profiles/roles/append.liquid new file mode 100644 index 0000000..aabfc15 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/roles/append.liquid @@ -0,0 +1,11 @@ +{% liquid + assign object = '{}' | parse_json | hash_merge: valid: true, id: id, role: role + function object = 'modules/core/commands/execute', object: object, mutation_name: 'modules/user/profiles/roles/append' + + if object.errors == blank + assign event_payload = null | hash_merge: profile_id: id, role: role + function _ = 'modules/core/commands/events/publish', type: 'user_role_appended', object: event_payload, delay: null, max_attempts: null + endif + + return object +%} diff --git a/modules/user/public/lib/commands/profiles/roles/remove.liquid b/modules/user/public/lib/commands/profiles/roles/remove.liquid new file mode 100644 index 0000000..2f6a6da --- /dev/null +++ b/modules/user/public/lib/commands/profiles/roles/remove.liquid @@ -0,0 +1,10 @@ +{% liquid + assign object = '{}' | parse_json | hash_merge: valid: true, id: id, role : role + function object = 'modules/core/commands/execute', object: object, mutation_name: 'modules/user/profiles/roles/remove' + + if object.errors == blank + assign event_payload = null | hash_merge: profile_id: id, role: role + function _ = 'modules/core/commands/events/publish', type: 'user_role_removed', object: event_payload, delay: null, max_attempts: null + endif + return object +%} diff --git a/modules/user/public/lib/commands/profiles/roles/set.liquid b/modules/user/public/lib/commands/profiles/roles/set.liquid new file mode 100644 index 0000000..c1a0f39 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/roles/set.liquid @@ -0,0 +1,10 @@ +{% liquid + assign object = '{}' | parse_json | hash_merge: valid: true, id: id, roles: roles + function object = 'modules/core/commands/execute', object: object, mutation_name: 'modules/user/profiles/roles/set' + + if object.errors == blank + assign event_payload = null | hash_merge: profile_id: id, roles: roles + function _ = 'modules/core/commands/events/publish', type: 'user_roles_set', object: event_payload, delay: null, max_attempts: null + endif + return object +%} diff --git a/modules/user/public/lib/commands/profiles/tokenize_names.liquid b/modules/user/public/lib/commands/profiles/tokenize_names.liquid new file mode 100644 index 0000000..f4b04c8 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/tokenize_names.liquid @@ -0,0 +1,4 @@ +{% liquid + assign tokenized_names = '[]' | parse_json | array_add: object.email | array_add: object.first_name | array_add: object.last_name | compact | uniq | join: ' ' | downcase + return tokenized_names +%} diff --git a/modules/user/public/lib/commands/profiles/update.liquid b/modules/user/public/lib/commands/profiles/update.liquid new file mode 100644 index 0000000..172bfa1 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/update.liquid @@ -0,0 +1,10 @@ +{% liquid + function object = 'modules/user/commands/profiles/update/build', object: object, profile: profile, name: null + function object = 'modules/user/commands/profiles/update/check', object: object + + if object.valid + function object = 'modules/core/commands/execute', mutation_name: 'modules/user/profiles/update', object: object + assign object = object | hash_merge: object.properties + endif + return object +%} diff --git a/modules/user/public/lib/commands/profiles/update/build.liquid b/modules/user/public/lib/commands/profiles/update/build.liquid new file mode 100644 index 0000000..43f68e6 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/update/build.liquid @@ -0,0 +1,7 @@ +{% liquid + function tokenized_names = 'modules/user/commands/profiles/tokenize_names', object: object + assign name = object.first_name | append: ' ' | append: object.last_name + assign data = null | hash_merge: id: profile.id, first_name: object.first_name, last_name: object.last_name, name: name, c__names: tokenized_names, email: profile.email + + return data +%} diff --git a/modules/user/public/lib/commands/profiles/update/check.liquid b/modules/user/public/lib/commands/profiles/update/check.liquid new file mode 100644 index 0000000..4704506 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/update/check.liquid @@ -0,0 +1,14 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'id' + function c = 'modules/core/validations/uniqueness', c: c, object: object, field_name: 'email', table: 'modules/user/profile', scope_name: null, exclude_name: null + function c = 'modules/core/validations/length', c: c, object: object, field_name: 'name', maximum: 80, allow_blank: null + function c = 'modules/core/validations/length', c: c, object: object, field_name: 'first_name', maximum: 40, allow_blank: null + function c = 'modules/core/validations/length', c: c, object: object, field_name: 'last_name', maximum: 40, allow_blank: null + + + assign object = object | hash_merge: c + + return object +%} diff --git a/modules/user/public/lib/commands/profiles/update_proxy.liquid b/modules/user/public/lib/commands/profiles/update_proxy.liquid new file mode 100644 index 0000000..be00e86 --- /dev/null +++ b/modules/user/public/lib/commands/profiles/update_proxy.liquid @@ -0,0 +1,6 @@ +{% liquid + function profile_module = 'modules/core/queries/variable/get', name: 'PROFILE_MODULE', default: 'modules/user', type: null + assign function_name = profile_module | append: '/commands/profiles/update' + function profile = function_name, object: object, profile: profile + return profile +%} diff --git a/modules/user/public/lib/commands/session/create.liquid b/modules/user/public/lib/commands/session/create.liquid new file mode 100644 index 0000000..4d99513 --- /dev/null +++ b/modules/user/public/lib/commands/session/create.liquid @@ -0,0 +1,55 @@ +{% comment %} + Creates a user sessions. + Params: + - user_id string (optional) + - validate_password boolean + default: true + - email: string (optional) + the user's email address + - password: string (optional) + the user's password + - hook_params: object + the other params that will be passed to hook_user_login + - skip_otp boolean + default: false +{% endcomment %} +{% liquid + if validate_password == nil + assign validate_password = true + endif + + if skip_otp == nil + assign skip_otp = false + endif + + if validate_password + function user = 'modules/user/commands/user/verify_password', email: email, password: password + unless user.valid + return user + endunless + assign user_id = user.id + endif + + function object = 'modules/user/commands/session/create/build', id: user_id + function object = 'modules/user/commands/session/create/check', object: object + if object.valid + function user = 'modules/user/queries/user/load', id: object.id + function profile = 'modules/user/queries/profiles/find', user_id: user.id, id: null, uuid: null, first_name: null, last_name: null + if profile.otp_configured and skip_otp == false + hash_assign object['otp_required'] = true + else + if user.id + sign_in user_id: user.id + assign params = '{}' | parse_json | hash_merge: user: user, hook_params: hook_params + function results = 'modules/core/commands/hook/fire', hook: 'user_login', params: params, merge_to_object: true + hash_assign user['hook_results'] = results + endif + hash_assign object['user'] = user + + assign event_payload = null | hash_merge: user_id: object.id + function _ = 'modules/core/commands/events/publish', type: 'user_signed_in', object: event_payload, delay: null, max_attempts: null + endif + endif + + return object +%} diff --git a/modules/user/public/lib/commands/session/create/build.liquid b/modules/user/public/lib/commands/session/create/build.liquid new file mode 100644 index 0000000..3cefb8b --- /dev/null +++ b/modules/user/public/lib/commands/session/create/build.liquid @@ -0,0 +1,7 @@ +{% parse_json object %} + { + "id": {{ id | json }} + } +{% endparse_json %} + +{% return object %} diff --git a/modules/user/public/lib/commands/session/create/check.liquid b/modules/user/public/lib/commands/session/create/check.liquid new file mode 100644 index 0000000..a8e6310 --- /dev/null +++ b/modules/user/public/lib/commands/session/create/check.liquid @@ -0,0 +1,10 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'id' + + hash_assign object['valid'] = c.valid + hash_assign object['errors'] = c.errors + + return object +%} diff --git a/modules/user/public/lib/commands/session/destroy.liquid b/modules/user/public/lib/commands/session/destroy.liquid new file mode 100644 index 0000000..1ad11d0 --- /dev/null +++ b/modules/user/public/lib/commands/session/destroy.liquid @@ -0,0 +1,20 @@ +{% comment %} + Destroys the current user's session. +{% endcomment %} +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + + graphql destroy = 'modules/user/session/destroy' + hash_assign destroy['user'] = current_profile.user + + unless destroy.errors + assign params = '{}' | parse_json | hash_merge: destroy: destroy + function results = 'modules/core/commands/hook/fire', hook: 'user_logout', params: params, merge_to_object: null + hash_assign destroy['hook_results'] = results + assign event_payload = null | hash_merge: user_id: current_profile.user.id + function _ = 'modules/core/commands/events/publish', type: 'user_logout', object: event_payload, delay: null, max_attempts: null + session original_user_id = null + session return_to = null + endunless + return destroy +%} diff --git a/modules/user/public/lib/commands/session/impersonation/create.liquid b/modules/user/public/lib/commands/session/impersonation/create.liquid new file mode 100644 index 0000000..55e3b14 --- /dev/null +++ b/modules/user/public/lib/commands/session/impersonation/create.liquid @@ -0,0 +1,15 @@ +{% liquid + function object = 'modules/user/commands/session/impersonation/create/build', user: user, current_user_id: current_user_id + function object = 'modules/user/commands/session/impersonation/create/check', object: object + if object.valid + sign_in user_id: object.id + session original_user_id = object.current_user_id + + assign event_object = null | hash_merge: actor_id: object.current_user_id, target_id: object.user_id + function _ = 'modules/core/commands/events/publish', type: 'impersonation_started', object: event_object, delay: null, max_attempts: null + endif + + return object +%} + + diff --git a/modules/user/public/lib/commands/session/impersonation/create/build.liquid b/modules/user/public/lib/commands/session/impersonation/create/build.liquid new file mode 100644 index 0000000..a549d34 --- /dev/null +++ b/modules/user/public/lib/commands/session/impersonation/create/build.liquid @@ -0,0 +1,8 @@ +{% parse_json object %} + { + "id": {{ user.id | json }}, + "current_user_id": {{ current_user_id | json }} + } +{% endparse_json %} + +{% return object %} diff --git a/modules/user/public/lib/commands/session/impersonation/create/check.liquid b/modules/user/public/lib/commands/session/impersonation/create/check.liquid new file mode 100644 index 0000000..3298544 --- /dev/null +++ b/modules/user/public/lib/commands/session/impersonation/create/check.liquid @@ -0,0 +1,11 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'id' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'current_user_id' + + hash_assign object['valid'] = c.valid + hash_assign object['errors'] = c.errors + + return object +%} diff --git a/modules/user/public/lib/commands/session/impersonation/destroy.liquid b/modules/user/public/lib/commands/session/impersonation/destroy.liquid new file mode 100644 index 0000000..d50546a --- /dev/null +++ b/modules/user/public/lib/commands/session/impersonation/destroy.liquid @@ -0,0 +1,14 @@ +{% liquid + function object = 'modules/user/commands/session/impersonation/create/build', user: user, current_user_id: current_user_id + function object = 'modules/user/commands/session/impersonation/destroy/check', object: object + + if object.valid + sign_in user_id: object.id + session original_user_id = null + + assign event_object = null | hash_merge: actor_id: object.id , impersonated_user_id: object.current_user_id + function _ = 'modules/core/commands/events/publish', type: 'impersonation_ended', object: event_object, delay: null, max_attempts: null + endif + + return object +%} diff --git a/modules/user/public/lib/commands/session/impersonation/destroy/check.liquid b/modules/user/public/lib/commands/session/impersonation/destroy/check.liquid new file mode 100644 index 0000000..3298544 --- /dev/null +++ b/modules/user/public/lib/commands/session/impersonation/destroy/check.liquid @@ -0,0 +1,11 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'id' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'current_user_id' + + hash_assign object['valid'] = c.valid + hash_assign object['errors'] = c.errors + + return object +%} diff --git a/modules/user/public/lib/commands/user/create.liquid b/modules/user/public/lib/commands/user/create.liquid new file mode 100644 index 0000000..29331b6 --- /dev/null +++ b/modules/user/public/lib/commands/user/create.liquid @@ -0,0 +1,34 @@ +{% comment %} + Creates a user. + Params: + - first_name: string + user's first name + - last_name: string + user's last name + - email: string + user's email address + - password: string + user's password + - roles: array + user's roles +{% endcomment %} +{% liquid + function object = 'modules/user/commands/user/create/build', first_name: first_name, last_name: last_name, email: email, password: password, hook_params: hook_params, roles: roles + function object = 'modules/user/commands/user/create/check', object: object + if object.valid + function user = 'modules/core/commands/execute', mutation_name: 'modules/user/user/create', object: object, selection: 'user' + hash_assign object['user_id'] = user.id + + function profile = 'modules/user/commands/profiles/create', object: object + if profile.valid != true + return profile + endif + + assign event_payload = null | hash_merge: user_id: user.id + function _ = 'modules/core/commands/events/publish', type: 'user_created', object: event_payload, delay: null, max_attempts: null + + return user + endif + + return object +%} diff --git a/modules/user/public/lib/commands/user/create/build.liquid b/modules/user/public/lib/commands/user/create/build.liquid new file mode 100644 index 0000000..361605f --- /dev/null +++ b/modules/user/public/lib/commands/user/create/build.liquid @@ -0,0 +1,13 @@ +{% parse_json object %} + { + "first_name": {{ first_name | json }}, + "last_name": {{ last_name | json }}, + "email": {{ email | json }}, + "password": {{ password | json }}, + "hook_params": {{ hook_params | json }} + } +{% endparse_json %} + +{% hash_assign object['roles'] = roles %} + +{% return object %} diff --git a/modules/user/public/lib/commands/user/create/check.liquid b/modules/user/public/lib/commands/user/create/check.liquid new file mode 100644 index 0000000..ca33f4e --- /dev/null +++ b/modules/user/public/lib/commands/user/create/check.liquid @@ -0,0 +1,23 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'first_name' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'last_name' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'password' + function c = 'modules/core/validations/password_complexity', c: c, object: object, field_name: 'password', key: null + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'email', key: 'modules/user/validation.email.required' + function c = 'modules/core/validations/length', c: c, object: object, field_name: 'roles', minimum: 0, allow_blank: true + function c = 'modules/core/validations/email', c: c, object: object, field_name: 'email', key: 'modules/user/validation.email.format' + + if object.email != blank + graphql user_exists = 'modules/user/user/list', email: object.email + if user_exists.users.total_entries > 0 + assign message = 'modules/user/validation.user_exists' | t + function c = 'modules/core/helpers/register_error', contract: c, field_name: 'email', message: message + endif + endif + + hash_assign object['valid'] = c.valid + hash_assign object['errors'] = c.errors + + return object +%} diff --git a/modules/user/public/lib/commands/user/delete.liquid b/modules/user/public/lib/commands/user/delete.liquid new file mode 100644 index 0000000..31ba099 --- /dev/null +++ b/modules/user/public/lib/commands/user/delete.liquid @@ -0,0 +1,20 @@ +{% comment %} + Deletes a user. + Params: + - id: string + the user's id +{% endcomment %} +{% liquid + graphql user = 'modules/user/user/delete', id: id + + unless user.errors + assign params = '{}' | parse_json | hash_merge: user: user.user + function results = 'modules/core/commands/hook/fire', hook: 'user_delete', params: params, merge_to_object: null + hash_assign user['hook_results'] = results + + assign event_payload = null | hash_merge: user_id: id + function _ = 'modules/core/commands/events/publish', type: 'user_deleted', object: event_payload, delay: null, max_attempts: null + endunless + + return user +%} diff --git a/modules/user/public/lib/commands/user/email_update.liquid b/modules/user/public/lib/commands/user/email_update.liquid new file mode 100644 index 0000000..264a966 --- /dev/null +++ b/modules/user/public/lib/commands/user/email_update.liquid @@ -0,0 +1,10 @@ +{% liquid + function object = 'modules/user/commands/user/email_update/build', object: object, current_user: current_user + function object = 'modules/user/commands/user/email_update/check', object: object, current_user: current_user, c: c + + if object.valid + function object = 'modules/core/commands/execute', object: object, mutation_name: 'modules/user/user/email_update', selection: 'user' + endif + + return object +%} diff --git a/modules/user/public/lib/commands/user/email_update/build.liquid b/modules/user/public/lib/commands/user/email_update/build.liquid new file mode 100644 index 0000000..c557228 --- /dev/null +++ b/modules/user/public/lib/commands/user/email_update/build.liquid @@ -0,0 +1,5 @@ +{% liquid + assign object = null | hash_merge: id: current_user.id, email: object.email, password: object.password + + return object +%} diff --git a/modules/user/public/lib/commands/user/email_update/check.liquid b/modules/user/public/lib/commands/user/email_update/check.liquid new file mode 100644 index 0000000..ce68ee9 --- /dev/null +++ b/modules/user/public/lib/commands/user/email_update/check.liquid @@ -0,0 +1,23 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'email' + function c = 'modules/core/validations/email', c: c, object: object, field_name: 'email' + if object.email != blank and object.email != current_user.email + graphql emails_count = 'modules/user/user/emails_count', email: object.email | dig: 'users', 'total_entries' + if emails_count > 0 + render 'modules/core/helpers/register_error', contract: c, field_name: 'email', key: 'app.errors.taken' + endif + endif + + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'password' + if object.password + function r = 'modules/core/commands/execute', mutation_name: 'modules/user/user/verify_password_for_user_id' object: object, selection: 'users' + assign user = r.results.first + + function c = 'modules/core/validations/truthy', c: c, field_name: 'password', object: user.authenticate, key: 'app.errors.invalid_password' + endif + + assign object = object | hash_merge: valid: c.valid, errors: c.errors + + return object +%} diff --git a/modules/user/public/lib/commands/user/update.liquid b/modules/user/public/lib/commands/user/update.liquid new file mode 100644 index 0000000..d2502fa --- /dev/null +++ b/modules/user/public/lib/commands/user/update.liquid @@ -0,0 +1,39 @@ +{% comment %} + Updates a user. + Params: + - id: string + - email: string + the user's email address + - password: string + the user's password + - hook_params: object + the other params that will be passed to hook_user_update +{% endcomment %} +{% liquid + function object = 'modules/user/commands/user/update/build', id: id, email: email, password: password + function object = 'modules/user/commands/user/update/check', object: object + + if object.valid + graphql user = 'modules/user/user/update', args: object + hash_assign object['update_result'] = user + unless user.errors + assign params = '{}' | parse_json | hash_merge: updated_user: user.user, hook_params: hook_params + # todo: merge_to_object for hooks with the same name will overwrite the validity of previous results + function results = 'modules/core/commands/hook/fire', hook: 'user_update', params: params, merge_to_object: true + for result in results + # using the errors key for a quick workaround for now + if result[1].valid == false or result[1].errors != blank + hash_assign object['valid'] = false + hash_assign object['errors'] = result[1].errors + break + endif + endfor + hash_assign object['hook_results'] = results + endunless + + assign event_payload = null | hash_merge: user_id: object.id + function _ = 'modules/core/commands/events/publish', type: 'user_updated', object: event_payload, delay: null, max_attempts: null + endif + + return object +%} diff --git a/modules/user/public/lib/commands/user/update/build.liquid b/modules/user/public/lib/commands/user/update/build.liquid new file mode 100644 index 0000000..978272d --- /dev/null +++ b/modules/user/public/lib/commands/user/update/build.liquid @@ -0,0 +1,9 @@ +{% parse_json object %} + { + "id": {{ id | json }}, + "email": {{ email | json }}, + "password": {{ password | json }} + } +{% endparse_json %} + +{% return object %} diff --git a/modules/user/public/lib/commands/user/update/check.liquid b/modules/user/public/lib/commands/user/update/check.liquid new file mode 100644 index 0000000..3719f9e --- /dev/null +++ b/modules/user/public/lib/commands/user/update/check.liquid @@ -0,0 +1,12 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + if object['email'] + function c = 'modules/core/validations/email', c: c, object: object, field_name: 'email' + endif + + hash_assign object['valid'] = c.valid + hash_assign object['errors'] = c.errors + + return object +%} diff --git a/modules/user/public/lib/commands/user/verify_otp.liquid b/modules/user/public/lib/commands/user/verify_otp.liquid new file mode 100644 index 0000000..cfc4752 --- /dev/null +++ b/modules/user/public/lib/commands/user/verify_otp.liquid @@ -0,0 +1,6 @@ +{% liquid + function object = 'modules/user/commands/user/verify_otp/build', object: object, email: email + function object = 'modules/user/commands/user/verify_otp/check', object: object + + return object +%} diff --git a/modules/user/public/lib/commands/user/verify_otp/build.liquid b/modules/user/public/lib/commands/user/verify_otp/build.liquid new file mode 100644 index 0000000..0762a45 --- /dev/null +++ b/modules/user/public/lib/commands/user/verify_otp/build.liquid @@ -0,0 +1,11 @@ +{% parse_json object %} + { + "email": {{ email | default: object.email | json }}, + "password": {{ object.password | json }}, + "otp_code": {{ object.otp_code | json }} + } +{% endparse_json %} + +{% liquid + return object +%} diff --git a/modules/user/public/lib/commands/user/verify_otp/check.liquid b/modules/user/public/lib/commands/user/verify_otp/check.liquid new file mode 100644 index 0000000..37151c6 --- /dev/null +++ b/modules/user/public/lib/commands/user/verify_otp/check.liquid @@ -0,0 +1,21 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'email' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'password' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'otp_code' + + if c.valid + function r = 'modules/core/commands/execute', mutation_name: 'modules/user/user/verify_otp' object: object, selection: 'users' + assign user = r.results.first + + function c = 'modules/core/validations/truthy', c: c, field_name: 'otp_code', object: user.authenticate, key: 'modules/user/2fa.errors.otp_code' + function c = 'modules/core/validations/truthy', c: c, field_name: 'password', object: user.authenticate, key: 'modules/user/2fa.errors.password' + hash_assign object['id'] = user.id + endif + + hash_assign object['valid'] = c.valid + hash_assign object['errors'] = c.errors + + return object +%} diff --git a/modules/user/public/lib/commands/user/verify_password.liquid b/modules/user/public/lib/commands/user/verify_password.liquid new file mode 100644 index 0000000..58bdd42 --- /dev/null +++ b/modules/user/public/lib/commands/user/verify_password.liquid @@ -0,0 +1,13 @@ +{% comment %} + Verifies a user's password. + Params: + - email: string + the user's email address + - password: string +{% endcomment %} +{% liquid + function object = 'modules/user/commands/user/verify_password/build', email: email, password: password + function object = 'modules/user/commands/user/verify_password/check', object: object + + return object +%} diff --git a/modules/user/public/lib/commands/user/verify_password/build.liquid b/modules/user/public/lib/commands/user/verify_password/build.liquid new file mode 100644 index 0000000..f1bbf6c --- /dev/null +++ b/modules/user/public/lib/commands/user/verify_password/build.liquid @@ -0,0 +1,10 @@ +{% parse_json object %} + { + "email": {{ email | json }}, + "password": {{ password | json }} + } +{% endparse_json %} + +{% liquid + return object +%} diff --git a/modules/user/public/lib/commands/user/verify_password/check.liquid b/modules/user/public/lib/commands/user/verify_password/check.liquid new file mode 100644 index 0000000..19a55b6 --- /dev/null +++ b/modules/user/public/lib/commands/user/verify_password/check.liquid @@ -0,0 +1,20 @@ +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/email', c: c, object: object, field_name: 'email' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'email' + function c = 'modules/core/validations/presence', c: c, object: object, field_name: 'password' + + if c.valid + graphql user = 'modules/user/user/verify_password', args: object + assign user = user.users.results.first + + function c = 'modules/core/validations/truthy', c: c, field_name: 'password', object: user.authenticate, key: 'modules/user/validation.invalid_email_or_password' + hash_assign object['id'] = user.id + endif + + hash_assign object['valid'] = c.valid + hash_assign object['errors'] = c.errors + + return object +%} diff --git a/modules/user/public/lib/events/authentication_link_created.liquid b/modules/user/public/lib/events/authentication_link_created.liquid new file mode 100644 index 0000000..6a9dfa1 --- /dev/null +++ b/modules/user/public/lib/events/authentication_link_created.liquid @@ -0,0 +1,12 @@ +--- +metadata: + event: + email +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'email' + + return c +%} diff --git a/modules/user/public/lib/events/impersonation_ended.liquid b/modules/user/public/lib/events/impersonation_ended.liquid new file mode 100644 index 0000000..0281e88 --- /dev/null +++ b/modules/user/public/lib/events/impersonation_ended.liquid @@ -0,0 +1,14 @@ +--- +metadata: + event: + actor_id + impersonation_ended +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'actor_id' + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'impersonation_ended' + + return c +%} diff --git a/modules/user/public/lib/events/impersonation_started.liquid b/modules/user/public/lib/events/impersonation_started.liquid new file mode 100644 index 0000000..258fb32 --- /dev/null +++ b/modules/user/public/lib/events/impersonation_started.liquid @@ -0,0 +1,16 @@ +--- +metadata: + event: + actor_id + target_id + +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'actor_id' + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'target_id' + + + return c +%} diff --git a/modules/user/public/lib/events/password_created.liquid b/modules/user/public/lib/events/password_created.liquid new file mode 100644 index 0000000..427444a --- /dev/null +++ b/modules/user/public/lib/events/password_created.liquid @@ -0,0 +1,12 @@ +--- +metadata: + event: + user_id +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'user_id' + + return c +%} diff --git a/modules/user/public/lib/events/user_created.liquid b/modules/user/public/lib/events/user_created.liquid new file mode 100644 index 0000000..427444a --- /dev/null +++ b/modules/user/public/lib/events/user_created.liquid @@ -0,0 +1,12 @@ +--- +metadata: + event: + user_id +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'user_id' + + return c +%} diff --git a/modules/user/public/lib/events/user_deleted.liquid b/modules/user/public/lib/events/user_deleted.liquid new file mode 100644 index 0000000..427444a --- /dev/null +++ b/modules/user/public/lib/events/user_deleted.liquid @@ -0,0 +1,12 @@ +--- +metadata: + event: + user_id +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'user_id' + + return c +%} diff --git a/modules/user/public/lib/events/user_logout.liquid b/modules/user/public/lib/events/user_logout.liquid new file mode 100644 index 0000000..427444a --- /dev/null +++ b/modules/user/public/lib/events/user_logout.liquid @@ -0,0 +1,12 @@ +--- +metadata: + event: + user_id +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'user_id' + + return c +%} diff --git a/modules/user/public/lib/events/user_role_appended.liquid b/modules/user/public/lib/events/user_role_appended.liquid new file mode 100644 index 0000000..a0936bb --- /dev/null +++ b/modules/user/public/lib/events/user_role_appended.liquid @@ -0,0 +1,14 @@ +--- +metadata: + event: + profile_id + role +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'profile_id' + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'role' + + return c +%} diff --git a/modules/user/public/lib/events/user_role_removed.liquid b/modules/user/public/lib/events/user_role_removed.liquid new file mode 100644 index 0000000..a0936bb --- /dev/null +++ b/modules/user/public/lib/events/user_role_removed.liquid @@ -0,0 +1,14 @@ +--- +metadata: + event: + profile_id + role +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'profile_id' + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'role' + + return c +%} diff --git a/modules/user/public/lib/events/user_roles_set.liquid b/modules/user/public/lib/events/user_roles_set.liquid new file mode 100644 index 0000000..65253d7 --- /dev/null +++ b/modules/user/public/lib/events/user_roles_set.liquid @@ -0,0 +1,14 @@ +--- +metadata: + event: + profile_id + roles +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'profile_id' + function c = 'modules/core/validations/length', c: c, object: event, field_name: 'roles', minimum: 0, allow_blank: false + + return c +%} diff --git a/modules/user/public/lib/events/user_signed_in.liquid b/modules/user/public/lib/events/user_signed_in.liquid new file mode 100644 index 0000000..427444a --- /dev/null +++ b/modules/user/public/lib/events/user_signed_in.liquid @@ -0,0 +1,12 @@ +--- +metadata: + event: + user_id +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'user_id' + + return c +%} diff --git a/modules/user/public/lib/events/user_updated.liquid b/modules/user/public/lib/events/user_updated.liquid new file mode 100644 index 0000000..427444a --- /dev/null +++ b/modules/user/public/lib/events/user_updated.liquid @@ -0,0 +1,12 @@ +--- +metadata: + event: + user_id +--- +{% liquid + assign c = '{ "errors": {}, "valid": true }' | parse_json + + function c = 'modules/core/validations/presence', c: c, object: event, field_name: 'user_id' + + return c +%} diff --git a/modules/user/public/lib/helpers/can_do.liquid b/modules/user/public/lib/helpers/can_do.liquid new file mode 100644 index 0000000..1467f59 --- /dev/null +++ b/modules/user/public/lib/helpers/can_do.liquid @@ -0,0 +1,42 @@ +{% comment %} + Checks if the requester can do domething on the given entiry. + + Params: + - requester Object + The requester must have a string array field called 'roles' + - entity Object + - do String + - access_callback String + The callback function that checks the request. By default searches that the `requester` + has `do` permission or not. +{% endcomment %} +{% liquid + assign roles_type = requester.roles | type_of + unless roles_type == 'Array' + log requester, type: 'ERROR the requester does not have "roles" array in modules/user/helpers/can_do' + return false + endunless + + if access_callback + function can = access_callback, requester: requester, entity: entity, do: do + return can + endif + + function permissions = 'modules/user/queries/role_permissions/permissions' + assign found_permission = false + for role in requester.roles + if permissions[role] == null + assign msg = 'WARNING: the following role is not defined in modules/user/queries/role_permissions/permissions: ' | append: role + log requester, type: msg + endif + if role == 'superadmin' + assign found_permission = true + break + elsif permissions[role] contains do + assign found_permission = true + break + endif + endfor + + return found_permission + %} diff --git a/modules/user/public/lib/helpers/can_do_or_redirect.liquid b/modules/user/public/lib/helpers/can_do_or_redirect.liquid new file mode 100644 index 0000000..042bee2 --- /dev/null +++ b/modules/user/public/lib/helpers/can_do_or_redirect.liquid @@ -0,0 +1,23 @@ +{% comment %} + Redirects if the the requester does not have right to do something on the given entity. + + Params: + - requester Object + The requester must has a string array field called 'permissions' + - entity Object + - do String + - access_callback String + The callback function that checks the request. By default searches that the `requester` + has `do` permission or not. + - return_url String + The url where to redirect if the requester does not have right to do something in the given entity. +{% endcomment %} +{% liquid + function can = 'modules/user/helpers/can_do', requester: requester, entity: entity, do: do, access_callback: access_callback + unless can + assign return_url = return_url | default: '/' + + redirect_to return_url + break + endunless +%} diff --git a/modules/user/public/lib/helpers/can_do_or_unauthorized.liquid b/modules/user/public/lib/helpers/can_do_or_unauthorized.liquid new file mode 100644 index 0000000..def893d --- /dev/null +++ b/modules/user/public/lib/helpers/can_do_or_unauthorized.liquid @@ -0,0 +1,33 @@ +{% comment %} + Renders 401 if the the requester does not have right to do something on the given entity. + + Params: + - requester Object + The requester must have a string array field called 'permissions' + - entity Object + - do String + - redirect_anonymous_to_login Boolean + - anonymous_return_to + Only works if redirect_anonymous_to_login is set to true. This parameter sets context.session.return_to, which is used in /sessions/create endpoint after successful login + - access_callback String + The callback function that checks the request. By default searches that the `requester` + has `do` permission or not. + - forbidden_partial String + The partial to render if the requester is not authorized. If not set, just sets the response status to 403. +{% endcomment %} +{% liquid + function can = 'modules/user/helpers/can_do', requester: requester, entity: entity, do: do, access_callback: access_callback + unless can + if requester.roles contains 'anonymous' and redirect_anonymous_to_login + session return_to = anonymous_return_to | default: context.location.href + assign info = 'modules/user/authorization.redirect_anonymous_info' | t + include 'modules/core/helpers/redirect_to', url: '/sessions/new', info: info + else + response_status 403 + if forbidden_partial + include forbidden_partial + endif + break + endif + endunless +%} diff --git a/modules/user/public/lib/helpers/current_profile.liquid b/modules/user/public/lib/helpers/current_profile.liquid new file mode 100644 index 0000000..460352f --- /dev/null +++ b/modules/user/public/lib/helpers/current_profile.liquid @@ -0,0 +1,17 @@ +{% liquid + function user = 'modules/user/queries/user/current' + if user.id == null + assign current_profile = '{ "user": null, "roles": ["anonymous"] }' | parse_json + else + function current_profile = 'modules/user/queries/profiles/find', user_id: user.id, id: null, uuid: null, first_name: null, last_name: null + assign current_profile = current_profile | hash_merge: user: user + if current_profile.roles != null + hash_assign current_profile['roles'] = current_profile.roles | array_add: 'authenticated' + else + hash_assign current_profile['roles'] = '["authenticated"]' | parse_json + endif + endif + + export current_profile + return current_profile +%} diff --git a/modules/user/public/lib/helpers/flash.liquid b/modules/user/public/lib/helpers/flash.liquid new file mode 100644 index 0000000..5087333 --- /dev/null +++ b/modules/user/public/lib/helpers/flash.liquid @@ -0,0 +1,22 @@ +{% liquid + assign error = error + assign notice = notice + assign info = info + assign force_clear = force_clear | default: false + + if error + assign message = error | t + assign severity = 'error' + elsif notice + assign message = notice | t + assign severity = 'success' + elsif info + assign message = info | t + assign severity = 'info' + endif + assign flash = null | hash_merge: message: message, severity: severity, from: context.location.pathname, force_clear: force_clear + + function _ = 'modules/core/commands/session/set', key: 'sflash', value: flash + + return null +%} diff --git a/modules/user/public/lib/helpers/get_assigned_oauth_providers.liquid b/modules/user/public/lib/helpers/get_assigned_oauth_providers.liquid new file mode 100644 index 0000000..225b059 --- /dev/null +++ b/modules/user/public/lib/helpers/get_assigned_oauth_providers.liquid @@ -0,0 +1,7 @@ +{% liquid + function current_user = 'modules/user/queries/user/current' + graphql g = 'modules/user/oauth/find_by_user_id', user_id: current_user.id + + assign providers = g.records.results | map: 'provider' + return providers +%} \ No newline at end of file diff --git a/modules/user/public/lib/helpers/get_available_oauth_providers.liquid b/modules/user/public/lib/helpers/get_available_oauth_providers.liquid new file mode 100644 index 0000000..86ceef8 --- /dev/null +++ b/modules/user/public/lib/helpers/get_available_oauth_providers.liquid @@ -0,0 +1,26 @@ +{% liquid + assign available_providers = "{}" | parse_json + assign keys = context.constants | hash_keys + for item in keys + assign starts_with = item | start_with: "OAUTH2_" + assign ends_with = item | end_with: "_PROVIDER" + if starts_with and ends_with and context.constants[item] != null + assign provider_id = item | replace: "OAUTH2_", "" | replace: "_PROVIDER", "" | upcase + + assign provider_key = "OAUTH2_" | append: provider_id | append: "_PROVIDER" + assign name_key = "OAUTH2_" | append: provider_id | append: "_NAME" + assign client_id_key = "OAUTH2_" | append: provider_id | append: "_CLIENT_ID" + assign secret_value_key = "OAUTH2_" | append: provider_id | append: "_SECRET_VALUE" + + assign provider_data = "{}" | parse_json + hash_assign provider_data['key'] = context.constants[provider_key] + hash_assign provider_data['name'] = context.constants[name_key] + hash_assign provider_data['client_id'] = context.constants[client_id_key] + hash_assign provider_data['secret_value'] = context.constants[secret_value_key] + + hash_assign available_providers[provider_id] = provider_data + endif + endfor + + return available_providers + %} \ No newline at end of file diff --git a/modules/user/public/lib/helpers/profiles/slugs/build.liquid b/modules/user/public/lib/helpers/profiles/slugs/build.liquid new file mode 100644 index 0000000..187c39c --- /dev/null +++ b/modules/user/public/lib/helpers/profiles/slugs/build.liquid @@ -0,0 +1,8 @@ +{% liquid + assign profile = profile | default: current_profile + assign first_name = profile.first_name + assign last_name = profile.last_name + assign slug = first_name | append: '-' | append: last_name | append: '-' | append: profile.id | slugify + + return slug +%} diff --git a/modules/user/public/lib/helpers/table_name.liquid b/modules/user/public/lib/helpers/table_name.liquid new file mode 100644 index 0000000..2988af3 --- /dev/null +++ b/modules/user/public/lib/helpers/table_name.liquid @@ -0,0 +1,5 @@ +{% liquid + function profile_module = 'modules/core/queries/variable/get', name: 'PROFILE_MODULE', default: 'modules/user', type: null + + return profile_module | append: '/profile' +%} diff --git a/modules/user/public/lib/helpers/user_from_temporary_token.liquid b/modules/user/public/lib/helpers/user_from_temporary_token.liquid new file mode 100644 index 0000000..044fe7c --- /dev/null +++ b/modules/user/public/lib/helpers/user_from_temporary_token.liquid @@ -0,0 +1,17 @@ +{% liquid + if token == blank or email == blank + return null + endif + + function user = 'modules/user/queries/user/find', email: email + + if user + assign authenticated = token | is_token_valid: user.id + endif + + if user and authenticated + return user + else + return null + endif +%} diff --git a/modules/user/public/lib/hooks/hook_admin_page.liquid b/modules/user/public/lib/hooks/hook_admin_page.liquid new file mode 100644 index 0000000..62da040 --- /dev/null +++ b/modules/user/public/lib/hooks/hook_admin_page.liquid @@ -0,0 +1,29 @@ +{% comment %} + Implements hook_admin_page. +{% endcomment %} + +{% parse_json admin_pages %} +[ + { + "relative_path": "/admin/users", + "partial": "modules/user/admin_pages/list", + "menu": { + "title": "Users", + "link_attributes": {} + }, + "permission": "admin.users.manage" + }, + { + "relative_path": null, + "menu": { + "title": "Log out", + "link_attributes": { + "href": "/sessions/destroy" + } + }, + "permission": "sessions.destroy" + } +] +{% endparse_json %} + +{% return admin_pages %} diff --git a/modules/user/public/lib/hooks/hook_module_info.liquid b/modules/user/public/lib/hooks/hook_module_info.liquid new file mode 100644 index 0000000..cb6782c --- /dev/null +++ b/modules/user/public/lib/hooks/hook_module_info.liquid @@ -0,0 +1,15 @@ +{% comment %} + Implements hook_module_info. +{% endcomment %} +{% parse_json info %} +{ + "name": "<%= &name =%>", + "machine_name": "<%= &machine_name =%>", + "type": "<%= &type =%>", + "version": "<%= &version =%>" +} +{% endparse_json %} + +{% liquid + return info +%} diff --git a/modules/user/public/lib/queries/api_call.liquid b/modules/user/public/lib/queries/api_call.liquid new file mode 100644 index 0000000..0b4120d --- /dev/null +++ b/modules/user/public/lib/queries/api_call.liquid @@ -0,0 +1,23 @@ +{% comment %} + params: + - api_template + - data + - timeout + returns: + { + response { + status + body + } + errors { + message + } + } +{% endcomment %} +{%- liquid + graphql g = 'modules/user/api_call', api_template: api_template, data: data, timeout: timeout + if g.api_call_send == blank + log g, type: "QUERY ERROR" + endif + return g.api_call_send +-%} diff --git a/modules/user/public/lib/queries/profiles/filters.liquid b/modules/user/public/lib/queries/profiles/filters.liquid new file mode 100644 index 0000000..0e490b4 --- /dev/null +++ b/modules/user/public/lib/queries/profiles/filters.liquid @@ -0,0 +1,15 @@ +{% parse_json sort_options %} +{ + "first_name_desc": { "properties": { "name": "first_name", "order": "DESC" }}, + "first_name_desc": { "properties": { "name": "first_name", "order": "ASC" }} +} +{% endparse_json %} +{% liquid + assign filters = '{}' | parse_json + hash_assign filters['page'] = params.page | to_positive_integer: 1 + hash_assign filters['keyword'] = params.keyword | default: '' + hash_assign filters['sort_by'] = params.sort_by | default: 'first_name_desc' + hash_assign filters['sort'] = sort_options[filters.sort_by] + + return filters +%} diff --git a/modules/user/public/lib/queries/profiles/filters_proxy.liquid b/modules/user/public/lib/queries/profiles/filters_proxy.liquid new file mode 100644 index 0000000..17420a8 --- /dev/null +++ b/modules/user/public/lib/queries/profiles/filters_proxy.liquid @@ -0,0 +1,7 @@ +{% liquid + function profile_module = 'modules/core/queries/variable/get', name: 'PROFILE_MODULE', default: 'modules/user', type: null + assign function_name = profile_module | append: '/queries/profiles/filters' + function filters = function_name, params: params + + return filters +%} diff --git a/modules/user/public/lib/queries/profiles/find.liquid b/modules/user/public/lib/queries/profiles/find.liquid new file mode 100644 index 0000000..9c64092 --- /dev/null +++ b/modules/user/public/lib/queries/profiles/find.liquid @@ -0,0 +1,16 @@ +{% liquid + if user_id == blank and id == blank and uuid == blank + log 'ERROR: missing ID argument in modules/user/queries/profile/find' + return nil + endif + + graphql result = 'modules/user/profiles/search', user_id: user_id, id: id, first_name: first_name, last_name: last_name, uuid: uuid, limit: 1, page: 1 + assign profile = result.records.results.first + + if profile + function slug = 'modules/user/helpers/profiles/slugs/build', current_profile: profile + hash_assign profile['slug'] = slug + endif + + return profile +%} diff --git a/modules/user/public/lib/queries/profiles/find_proxy.liquid b/modules/user/public/lib/queries/profiles/find_proxy.liquid new file mode 100644 index 0000000..69f73e0 --- /dev/null +++ b/modules/user/public/lib/queries/profiles/find_proxy.liquid @@ -0,0 +1,6 @@ +{% liquid + function profile_module = 'modules/core/queries/variable/get', name: 'PROFILE_MODULE', default: 'modules/user', type: null + assign function_name = profile_module | append: '/queries/profiles/find' + function profile = function_name, user_id: user_id, id: id, uuid: uuid, filters: filters + return profile +%} diff --git a/modules/user/public/lib/queries/profiles/search.liquid b/modules/user/public/lib/queries/profiles/search.liquid new file mode 100644 index 0000000..ec4ed53 --- /dev/null +++ b/modules/user/public/lib/queries/profiles/search.liquid @@ -0,0 +1,22 @@ +{% liquid + assign page = page | to_positive_integer: 1 + if not_ids == blank + assign not_ids = null + endif + + graphql r = 'modules/user/profiles/search', limit: limit, uuid: uuid, id: id, ids: ids, first_name: first_name , last_name: last_name , user_id: user_id, not_ids: not_ids, query: query, emails: emails, sort: sort, page: page + + assign records = r.records + assign profiles = '[]' | parse_json + for profile in records.results + function slug = 'modules/user/helpers/profiles/slugs/build' , current_profile: profile + hash_assign profile['slug'] = slug + assign p = profile + + assign profiles = profiles | array_add: p + endfor + hash_assign records['results'] = profiles + + return records +%} + diff --git a/modules/user/public/lib/queries/profiles/search_proxy.liquid b/modules/user/public/lib/queries/profiles/search_proxy.liquid new file mode 100644 index 0000000..8eb03dc --- /dev/null +++ b/modules/user/public/lib/queries/profiles/search_proxy.liquid @@ -0,0 +1,7 @@ +{% liquid + function profile_module = 'modules/core/queries/variable/get', name: 'PROFILE_MODULE', default: 'modules/user', type: null + assign function_name = profile_module | append: '/queries/profiles/search' + function profile = function_name, limit: limit, uuid: uuid, id: id, ids: ids, first_name: first_name, last_name: last_name, user_id: user_id, not_ids: not_ids, query: query, emails: emails, sort: sort, page: page, filters: filters + + return profile +%} diff --git a/modules/user/public/lib/queries/registration_fields/load.liquid b/modules/user/public/lib/queries/registration_fields/load.liquid new file mode 100644 index 0000000..7c8c4a1 --- /dev/null +++ b/modules/user/public/lib/queries/registration_fields/load.liquid @@ -0,0 +1,19 @@ +{% comment %} + Loads the registration fields. +{% endcomment %} + +{% parse_json fields %} +[ + {% comment %} + Example value: + { + "name": "email", + "type": "email", + "label": "Email" + } + {% endcomment %} +] +{% endparse_json %} +{% liquid + return fields +%} diff --git a/modules/user/public/lib/queries/role_permissions/permissions.liquid b/modules/user/public/lib/queries/role_permissions/permissions.liquid new file mode 100644 index 0000000..5557577 --- /dev/null +++ b/modules/user/public/lib/queries/role_permissions/permissions.liquid @@ -0,0 +1,14 @@ +{% parse_json data %} +{ + {% if context.constants.USER_DEFAULT_ROLE != blank %} + "{{ context.constants.USER_DEFAULT_ROLE }}": [], + {% endif %} + "anonymous": ["sessions.create", "users.register"], + "authenticated": ["sessions.destroy","oauth.manage"], + "admin": ["admin_pages.view", "admin.users.manage", "users.impersonate"], + "member": ["profile.manage"], + "superadmin": ["users.impersonate_superadmin"] +} +{% endparse_json %} + +{% return data %} diff --git a/modules/user/public/lib/queries/roles/all.liquid b/modules/user/public/lib/queries/roles/all.liquid new file mode 100644 index 0000000..0d481bb --- /dev/null +++ b/modules/user/public/lib/queries/roles/all.liquid @@ -0,0 +1,3 @@ +{% function permissions = 'modules/user/queries/role_permissions/permissions' %} + +{% return permissions | hash_keys | sort %} diff --git a/modules/user/public/lib/queries/roles/custom.liquid b/modules/user/public/lib/queries/roles/custom.liquid new file mode 100644 index 0000000..bebd8aa --- /dev/null +++ b/modules/user/public/lib/queries/roles/custom.liquid @@ -0,0 +1,6 @@ +{% liquid + function roles = 'modules/user/queries/roles/all' + assign built_in_roles = 'anonymous,authenticated' | split: ',' + + return roles | array_subtract: built_in_roles +%} diff --git a/modules/user/public/lib/queries/user/count.liquid b/modules/user/public/lib/queries/user/count.liquid new file mode 100644 index 0000000..3c8098a --- /dev/null +++ b/modules/user/public/lib/queries/user/count.liquid @@ -0,0 +1,7 @@ +{% comment %} + Counts the created users +{% endcomment %} +{% liquid + graphql count = 'modules/user/user/count' + return count.users.total_entries +%} diff --git a/modules/user/public/lib/queries/user/current.liquid b/modules/user/public/lib/queries/user/current.liquid new file mode 100644 index 0000000..80c343a --- /dev/null +++ b/modules/user/public/lib/queries/user/current.liquid @@ -0,0 +1,11 @@ +{% comment %} + Loads the current user. +{% endcomment %} +{% liquid + if context.current_user + function user = 'modules/user/queries/user/load', id: context.current_user.id + else + assign user = null + endif + return user +%} diff --git a/modules/user/public/lib/queries/user/find.liquid b/modules/user/public/lib/queries/user/find.liquid new file mode 100644 index 0000000..f121e76 --- /dev/null +++ b/modules/user/public/lib/queries/user/find.liquid @@ -0,0 +1,13 @@ +{% liquid + assign id = id + assign email = email + assign with_token = with_token | default: false + + if id == blank and email == blank + return null + endif + + graphql r = 'modules/user/user/find', id: id, email: email, limit: 1, with_token: with_token + + return r.users.results.first +%} diff --git a/modules/user/public/lib/queries/user/get_all.liquid b/modules/user/public/lib/queries/user/get_all.liquid new file mode 100644 index 0000000..a700388 --- /dev/null +++ b/modules/user/public/lib/queries/user/get_all.liquid @@ -0,0 +1,6 @@ +{% liquid + # log 1, type: 'user/list' + graphql g = 'modules/user/user/list' + + return g.users.results +%} diff --git a/modules/user/public/lib/queries/user/load.liquid b/modules/user/public/lib/queries/user/load.liquid new file mode 100644 index 0000000..efdb00a --- /dev/null +++ b/modules/user/public/lib/queries/user/load.liquid @@ -0,0 +1,13 @@ +{% comment %} + Loads a user's data. + Params: + - id: string + the user's id +{% endcomment %} +{% liquid + + graphql g = 'modules/user/user/load', id: id + assign user = g.users.results.first + + return user +%} diff --git a/modules/user/public/lib/queries/user/otp.liquid b/modules/user/public/lib/queries/user/otp.liquid new file mode 100644 index 0000000..fef9947 --- /dev/null +++ b/modules/user/public/lib/queries/user/otp.liquid @@ -0,0 +1,17 @@ +{% liquid + if email == blank + log 'Something went wrong. Email cannot be blank.', type: 'ERROR' + return null + endif + + assign issuer = 'app.title' | t + graphql r = 'modules/user/user/otp', email: email, issuer: issuer + + if r.errors + assign type = 'ERROR' | append: name + log r, type: type + break + endif + + return r.users.results.first +%} diff --git a/modules/user/public/lib/queries/user/search.liquid b/modules/user/public/lib/queries/user/search.liquid new file mode 100644 index 0000000..0eda928 --- /dev/null +++ b/modules/user/public/lib/queries/user/search.liquid @@ -0,0 +1,7 @@ +{% liquid + assign limit = limit | default: 20 + assign page = page | to_positive_integer: 1 + + graphql r = 'modules/user/user/search', id: id, not_ids: not_ids, email: email, page: page, limit: limit, sort: sort + return r.users +%} diff --git a/modules/user/public/schema/oauth.yml b/modules/user/public/schema/oauth.yml new file mode 100644 index 0000000..2e88f77 --- /dev/null +++ b/modules/user/public/schema/oauth.yml @@ -0,0 +1,8 @@ +name: oauth +properties: + - name: user_id + type: string + - name: provider + type: string + - name: sub + type: string \ No newline at end of file diff --git a/modules/user/public/schema/profile.yml b/modules/user/public/schema/profile.yml new file mode 100644 index 0000000..1cdf1f8 --- /dev/null +++ b/modules/user/public/schema/profile.yml @@ -0,0 +1,17 @@ +name: profile +properties: + - name: uuid + - name: user_id + - name: name + - name: first_name + - name: last_name + - name: email # duplication from user + - name: roles + type: array + + # tokenized downcased names for searching + - name: c__names + + # 2fa + - name: otp_configured + type: boolean diff --git a/modules/user/public/translations/en/2fa.yml b/modules/user/public/translations/en/2fa.yml new file mode 100644 index 0000000..1de92a2 --- /dev/null +++ b/modules/user/public/translations/en/2fa.yml @@ -0,0 +1,25 @@ +en: + 2fa: + errors: + otp_code: Invalid OTP code provided. + password: Invalid password provided. + new: + enter_password: Enter password + your_password: Please enter your password. + submit: Submit + confirm_and_enable: Confirm and Enable Two Factor + two_factor_authentication: Two-factor authentication + scan_qr_code: Scan QR Code + 2fa_info: |- + To be able to log in you need to scan this QR Code with your authentication app. + + You can use [Authy](https://authy.com) or [any other authentication tool](https://www.lastpass.com/two-factor-authentication). + if_you_cannot_scan: 'Or enter the following code manually in the app:' + confirm_otp_code: Confirm code + please_confirm: Enter 6-digit code from your two-factor authenticator app + disable: + two_factor_authentication: Disable two factor authentication + create: + success: Successfully enabled two factor authentication. + delete: + success: Successfully disabled two factor authentication. diff --git a/modules/user/public/translations/en/authentication_links.yml b/modules/user/public/translations/en/authentication_links.yml new file mode 100644 index 0000000..3f0000e --- /dev/null +++ b/modules/user/public/translations/en/authentication_links.yml @@ -0,0 +1,6 @@ +en: + authentication_links: + created: + Please check your inbox. If the provided email was correct, you'll + receive some instructions on how to reset your password. + something_went_wrong: Something went wrong diff --git a/modules/user/public/translations/en/authorization.yml b/modules/user/public/translations/en/authorization.yml new file mode 100644 index 0000000..b291143 --- /dev/null +++ b/modules/user/public/translations/en/authorization.yml @@ -0,0 +1,3 @@ +en: + authorization: + redirect_anonymous_info: 'Please log in to access this page.' diff --git a/modules/user/public/translations/en/emails.yml b/modules/user/public/translations/en/emails.yml new file mode 100644 index 0000000..3a312a8 --- /dev/null +++ b/modules/user/public/translations/en/emails.yml @@ -0,0 +1,14 @@ +en: + emails: + from_email: noreply@platformos.com + passwords: + reset: + subject: Reset password + title: Password reset request + cta: Reset password + cta_button: Go to reset password form + content: It seems that you requested a password reset. To proceed use the + following button. + ignore: If it wasn’t you who requested this just ignore this message. + + diff --git a/modules/user/public/translations/en/oauth.yml b/modules/user/public/translations/en/oauth.yml new file mode 100644 index 0000000..c57af4d --- /dev/null +++ b/modules/user/public/translations/en/oauth.yml @@ -0,0 +1,13 @@ +en: + oauth: + app: + no_providers_available: 'There are no providers available' + provider_already_assigned: 'Selected provider is already in use.' + sub_already_assigned: 'External account is already connected to a different user.' + signed_in: 'Successfully signed in using OAuth 2.' + assigned_provider: 'Successfully assigned OAuth 2 provider.' + unassigned_provider: 'Successfully unassigned OAuth 2 provider.' + failed_to_create_account: 'Could not create a new account. Please try again later.' + invalid_request: 'Invalid request.' + unassign_provider: 'Unassign' + user_info_error: 'Could not fetch user info from OAuth 2 provider. Please try again later.' \ No newline at end of file diff --git a/modules/user/public/translations/en/passwords.yml b/modules/user/public/translations/en/passwords.yml new file mode 100644 index 0000000..0ffe4f4 --- /dev/null +++ b/modules/user/public/translations/en/passwords.yml @@ -0,0 +1,17 @@ +en: + passwords: + password: Password + password_confirmation: Confirm password + password_update: Update password + new_password: New password + confirm_new_password: Confirm new password + login: Login + register: Register + reset_password: Email an authentication link + edit: Reset Password + email: Email + email_desc: Enter your registered email and we will send you a link to reset your password + reset_password_title: Reset password + expired_link: The reset password link you’ve entered is invalid or has expired. + remembered_password: You've remembered the password? + forgot: Forgot password? \ No newline at end of file diff --git a/modules/user/public/translations/en/sessions.yml b/modules/user/public/translations/en/sessions.yml new file mode 100644 index 0000000..d3c68fc --- /dev/null +++ b/modules/user/public/translations/en/sessions.yml @@ -0,0 +1,8 @@ +en: + sessions: + new: + log_in: Log In + dont_have_account: Don't have an account? + request_to_join: Request to join + social_login_separator: or + back_to_login: Return to login page diff --git a/modules/user/public/translations/en/users.yml b/modules/user/public/translations/en/users.yml new file mode 100644 index 0000000..8163d8e --- /dev/null +++ b/modules/user/public/translations/en/users.yml @@ -0,0 +1,12 @@ +en: + users: + new: + create_account: Create an Account + already_have_account: Already have an account? Log in + logout: Log out + email: + new_email: 'New email' + current_password: 'Current password' + change_email: 'Change email' + email_update: 'Update' + change_success: 'Email updated' \ No newline at end of file diff --git a/modules/user/public/translations/en/validation.yml b/modules/user/public/translations/en/validation.yml new file mode 100644 index 0000000..07d8b3d --- /dev/null +++ b/modules/user/public/translations/en/validation.yml @@ -0,0 +1,18 @@ +en: + validation: + email: + required: Please provide your email address + format: The email doesn't look right, please check again + user_exists: It seems you already have a registered account. Please check the email field again or log in with your credentials. + taken: already taken + not_uniq: not unique + invalid_email_or_password: Invalid email or password + invalid_password: Invalid password. + matches: not valid format + not_truthy: not true + password: + lowercase: must include at least one lower case + uppercase: must include at least one upper case + number: must include at least one number + do_not_match: passwords do not match + invalid: invalid diff --git a/modules/user/public/views/pages/authentication_links/create.liquid b/modules/user/public/views/pages/authentication_links/create.liquid new file mode 100644 index 0000000..7a9663f --- /dev/null +++ b/modules/user/public/views/pages/authentication_links/create.liquid @@ -0,0 +1,33 @@ +--- +method: post +slug: authentication_links +--- +{% liquid + function object = 'modules/user/commands/authentication_links/create', email: context.params.authentication_link.email, host: context.location.host, hcaptcha_params: context.params, valid_for: null + if object.valid + function email = 'modules/user/commands/emails/auth-link', object: object + if email.valid + if object.email == 'change-password@example.com' and context.environment == 'staging' + echo object.url + break + endif + + function _ = 'modules/user/helpers/flash', notice: 'modules/user/authentication_links.created' + redirect_to '/' + else + log email.errors, type: 'ERROR: authentication_links/create email' + + function _ = 'modules/user/helpers/flash', notice: 'modules/user/authentication_links.something_went_wrong' + redirect_to '/' + endif + elsif object.token == blank + if context.environment == 'staging' + log object, type: 'DEBUG: reset-password-user-not-found' + endif + + function _ = 'modules/user/helpers/flash', notice: 'modules/user/authentication_links.created' + redirect_to '/' + else + render 'modules/user/passwords/reset', context: context, errors: object.errors, values: null + endif +%} diff --git a/modules/user/public/views/pages/oauth/callback.liquid b/modules/user/public/views/pages/oauth/callback.liquid new file mode 100644 index 0000000..e5615ca --- /dev/null +++ b/modules/user/public/views/pages/oauth/callback.liquid @@ -0,0 +1,88 @@ +--- +method: get +slug: oauth/:provider/callback +--- +{% liquid + # platformos-check-disable ConvertIncludeToRender + + function current_user = "modules/user/queries/user/current" + assign state = context.session.state + if context.params.code == blank or context.params.provider == blank or context.params.state != state + include 'modules/core/helpers/redirect_to', error: "modules/user/oauth.app.invalid_request" + return + endif + + function available_providers = "modules/user/helpers/get_available_oauth_providers" + assign provider = context.params.provider | upcase + assign selected_provider = available_providers[provider] + + if selected_provider == blank + include 'modules/core/helpers/redirect_to' + return + endif + + # check if user already has a given provider + if current_user.id != blank + graphql g = "modules/user/oauth/find_by_user_id", provider: provider, user_id: current_user.id + if g.records.total_entries > 0 + log "Provider already assigned", type: "ERROR" + include 'modules/core/helpers/redirect_to', notice: "modules/user/oauth.app.provider_already_assigned" + return + endif + endif + + # fetch user info using the appropriate module + assign command_path = "modules/oauth_" | append: selected_provider.key | append: "/helpers/get_user_info" + function user_info = command_path, provider: selected_provider, code: context.params.code + + if user_info.valid == false + include 'modules/core/helpers/redirect_to', notice: "modules/user/oauth.app.user_info_error" + return + endif + + assign user_sub = user_info.sub | json + assign user_email = user_info.email + assign user_first_name = user_info.first_name + assign user_last_name = user_info.last_name + + # check if sub is already registered to an existing user + graphql g = "modules/user/oauth/find_by_sub", provider: provider, sub: user_sub + assign found_user_id = current_user.id + assign create_provider_assignment = true + if g.records.total_entries > 0 + if current_user.id != null + log "Sub already assigned", type: "ERROR" + include 'modules/core/helpers/redirect_to', notice: "modules/user/oauth.app.sub_already_assigned" + return + else + assign found_user_id = g.records.results[0].user_id + assign create_provider_assignment = false + endif + endif + + # check if user account should be created + if current_user.id == null and found_user_id == null + function new_user = "modules/user/commands/oauth/create_user", user_first_name: user_first_name, user_last_name: user_last_name, user_email: user_email + if new_user == null or new_user.valid == false + log new_user.errors, type: "ERROR" + include 'modules/core/helpers/redirect_to', notice: "modules/user/oauth.app.failed_to_create_account" + return + endif + assign found_user_id = new_user.id + endif + + # create a connection between user and provider + if create_provider_assignment + graphql g = "modules/user/oauth/create", sub: user_sub, provider: provider, user_id: found_user_id + endif + + # sign in as user + if current_user.id == blank + function _ = "modules/user/commands/session/create", user_id: found_user_id, validate_password: false, skip_otp: true, email: null, password: null, hook_params: null + include 'modules/core/helpers/redirect_to', notice: "modules/user/oauth.app.signed_in" + else + include 'modules/core/helpers/redirect_to', notice: "modules/user/oauth.app.assigned_provider" + endif + # platformos-check-enable ConvertIncludeToRender +%} + diff --git a/modules/user/public/views/pages/oauth/start.liquid b/modules/user/public/views/pages/oauth/start.liquid new file mode 100644 index 0000000..9356a65 --- /dev/null +++ b/modules/user/public/views/pages/oauth/start.liquid @@ -0,0 +1,27 @@ +--- +method: post +slug: oauth/:provider/start +--- + +{% liquid + if context.params.provider == blank + log "Provider not provided", type: "ERROR" + redirect_to '/' + endif + + function available_providers = 'modules/user/helpers/get_available_oauth_providers' + assign provider = context.params.provider | upcase + assign selected_provider = available_providers[provider] + + if selected_provider == blank + assign error = "Provider does not exist: " | append: provider + log error, type: "ERROR" + redirect_to '/' + endif + + session state = '' | uuid + + assign command_path = "modules/oauth_" | append: selected_provider.key | append: "/helpers/get_redirect_url" + function url = command_path, provider: selected_provider, state: context.session.state + redirect_to url +%} \ No newline at end of file diff --git a/modules/user/public/views/pages/oauth/unassign.liquid b/modules/user/public/views/pages/oauth/unassign.liquid new file mode 100644 index 0000000..3ff301b --- /dev/null +++ b/modules/user/public/views/pages/oauth/unassign.liquid @@ -0,0 +1,21 @@ +--- +slug: oauth/:provider/unassign +method: delete +--- +{% liquid + function current_user = 'modules/user/queries/user/current' + + if context.params.provider == blank or current_user.id == blank + redirect_to '/' + endif + + assign provider = context.params.provider | upcase + + graphql g = 'modules/user/oauth/find_by_user_id', provider: provider, user_id: current_user.id + if g.records.total_entries > 0 + graphql g = 'modules/user/oauth/delete', id: g.records.results[0].id + endif + + function _ = 'modules/user/helpers/flash', notice: 'modules/user/oauth.app.unassigned_provider' + redirect_to '/' +%} diff --git a/modules/user/public/views/pages/passwords/create.liquid b/modules/user/public/views/pages/passwords/create.liquid new file mode 100644 index 0000000..4a641f8 --- /dev/null +++ b/modules/user/public/views/pages/passwords/create.liquid @@ -0,0 +1,19 @@ +--- +slug: passwords +method: post +--- +{% liquid + assign input = context.params.password + assign redirect_url = context.params.redirect_to | default: '/' + hash_assign input['user_id'] = context.session.reset_password_session_user_id + + function object = 'modules/user/commands/passwords/create', object: input + if object.valid + session reset_password_session_user_id = null + + function _ = 'modules/user/commands/session/create', user_id: object.id, validate_password: false, email: null, password: null, hook_params: null, skip_otp: null + redirect_to redirect_url + else + render 'modules/user/passwords/new', context: context, errors: object.errors + endif +%} diff --git a/modules/user/public/views/pages/passwords/new.liquid b/modules/user/public/views/pages/passwords/new.liquid new file mode 100644 index 0000000..062e6b7 --- /dev/null +++ b/modules/user/public/views/pages/passwords/new.liquid @@ -0,0 +1,15 @@ +{% liquid + if context.session.reset_password_session_user_id == blank + function user = 'modules/user/helpers/user_from_temporary_token', token: context.params.token, email: context.params.email + + if user + session reset_password_session_user_id = user.id + else + function _ = 'modules/user/helpers/flash', error: 'modules/user/passwords.expired_link' + redirect_to '/sessions/new' + break + endif + endif + + render 'modules/user/passwords/new', context: context +%} diff --git a/modules/user/public/views/pages/passwords/reset.liquid b/modules/user/public/views/pages/passwords/reset.liquid new file mode 100644 index 0000000..14aae84 --- /dev/null +++ b/modules/user/public/views/pages/passwords/reset.liquid @@ -0,0 +1 @@ +{% render 'modules/user/passwords/reset', context: context, values: null %} diff --git a/modules/user/public/views/pages/profiles/2fa/create.liquid b/modules/user/public/views/pages/profiles/2fa/create.liquid new file mode 100644 index 0000000..e97d6a7 --- /dev/null +++ b/modules/user/public/views/pages/profiles/2fa/create.liquid @@ -0,0 +1,24 @@ +--- +slug: profiles/2fa +method: post +--- +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + if current_profile.id == blank + redirect_to '/' + endif + + function object = 'modules/user/commands/user/verify_otp', object: context.params.2fa, email: current_profile.user.email + if object.valid + hash_assign current_profile['otp_configured'] = true + function object = 'modules/user/commands/profiles/mark_otp', object: current_profile + if object.valid != true + log object, 'ERROR: modules/user/profiles/mark_otp' + endif + assign notice = 'modules/user/2fa.create.success' | t + include 'modules/core/helpers/redirect_to', url: '/', notice: notice + else + function user_otp = 'modules/user/queries/user/otp', email: current_profile.user.email, name: null + render 'modules/user/2fa/setup', otp: user_otp.otp, errors: object.errors, object: object + endif +%} diff --git a/modules/user/public/views/pages/profiles/2fa/delete.liquid b/modules/user/public/views/pages/profiles/2fa/delete.liquid new file mode 100644 index 0000000..970c0f9 --- /dev/null +++ b/modules/user/public/views/pages/profiles/2fa/delete.liquid @@ -0,0 +1,23 @@ +--- +slug: profiles/2fa/delete +method: post +--- +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + if current_profile.id == blank + redirect_to '/' + endif + + function object = 'modules/user/commands/user/verify_otp', object: context.params.2fa, email: current_profile.user.email + if object.valid + hash_assign current_profile['otp_configured'] = false + function object = 'modules/user/commands/profiles/mark_otp', object: current_profile + if object.valid != true + log object, 'ERROR: modules/user/profiles/mark_otp' + endif + assign notice = 'modules/user/2fa.delete.success' | t + include 'modules/core/helpers/redirect_to', url: '/', notice: notice + else + render 'modules/user/2fa/disable', errors: object.errors + endif +%} diff --git a/modules/user/public/views/pages/profiles/2fa/disable.liquid b/modules/user/public/views/pages/profiles/2fa/disable.liquid new file mode 100644 index 0000000..0ef75da --- /dev/null +++ b/modules/user/public/views/pages/profiles/2fa/disable.liquid @@ -0,0 +1,8 @@ +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + if current_profile.user.id == null or current_profile.otp_configured != true + redirect_to '/' + endif + + render 'modules/user/2fa/disable', errors: null +%} diff --git a/modules/user/public/views/pages/profiles/2fa/new.liquid b/modules/user/public/views/pages/profiles/2fa/new.liquid new file mode 100644 index 0000000..70c73db --- /dev/null +++ b/modules/user/public/views/pages/profiles/2fa/new.liquid @@ -0,0 +1,10 @@ +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + if current_profile.user.id == null + redirect_to '/' + endif + + function user_otp = 'modules/user/queries/user/otp', email: current_profile.user.email, name: null + + render 'modules/user/2fa/setup', otp: user_otp.otp, object: null, errors: null +%} diff --git a/modules/user/public/views/pages/sessions/2fa.liquid b/modules/user/public/views/pages/sessions/2fa.liquid new file mode 100644 index 0000000..35d5f70 --- /dev/null +++ b/modules/user/public/views/pages/sessions/2fa.liquid @@ -0,0 +1,23 @@ +--- +method: post +--- +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + if current_profile.user != null + include 'modules/core/helpers/redirect_to' + endif + + include 'modules/user/helpers/can_do_or_redirect', requester: current_profile, do: 'sessions.create', return_url: '/' + + function object = 'modules/user/commands/user/verify_otp', object: context.params.2fa, email: null + if object.valid + function res = 'modules/user/commands/session/create', email: object.email, password: object.password, hook_params: context.params, skip_otp: true, validate_password: null + if res.valid + include 'modules/core/helpers/redirect_to' + else + render 'modules/user/sessions/new', context: context, errors: res.errors, values: null + endif + else + render 'modules/user/2fa/verify', object: object + endif +%} diff --git a/modules/user/public/views/pages/sessions/create.liquid b/modules/user/public/views/pages/sessions/create.liquid new file mode 100644 index 0000000..a99800b --- /dev/null +++ b/modules/user/public/views/pages/sessions/create.liquid @@ -0,0 +1,17 @@ +--- +method: post +slug: sessions +--- +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + + include 'modules/user/helpers/can_do_or_redirect', requester: current_profile, do: 'sessions.create', return_url: '/' + function res = 'modules/user/commands/session/create', email: context.params.email, password: context.params.password, hook_params: context.params, validate_password: true, skip_otp: null + if res.valid and res.otp_required + render 'modules/user/2fa/verify', object: context.params + elsif res.valid + include 'modules/core/helpers/redirect_to' + else + render 'modules/user/sessions/new', context: context, errors: res.errors, values: null + endif +%} diff --git a/modules/user/public/views/pages/sessions/destroy.liquid b/modules/user/public/views/pages/sessions/destroy.liquid new file mode 100644 index 0000000..e3c7512 --- /dev/null +++ b/modules/user/public/views/pages/sessions/destroy.liquid @@ -0,0 +1,14 @@ +--- +slug: sessions +method: delete +--- +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + + include 'modules/user/helpers/can_do_or_redirect', requester: current_profile, do: 'sessions.destroy', return_url: '/' + + function res = 'modules/user/commands/session/destroy' + + assign redirect_path = res.hook_results.redirect_to | default: params.redirect_to | default: '/' + redirect_to redirect_path +%} diff --git a/modules/user/public/views/pages/sessions/impersonation/create.liquid b/modules/user/public/views/pages/sessions/impersonation/create.liquid new file mode 100644 index 0000000..a9342b4 --- /dev/null +++ b/modules/user/public/views/pages/sessions/impersonation/create.liquid @@ -0,0 +1,30 @@ +--- +slug: sessions/impersonations +method: post +--- +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + function user_to_impersonate = 'modules/user/queries/user/load', id: context.params.user_id + + if user_to_impersonate + if user_to_impersonate.roles contains 'superadmin' + assign permission = 'users.impersonate_superadmin' + else + assign permission = 'users.impersonate' + endif + + # platformos-check-disable UnreachableCode + include 'modules/user/helpers/can_do_or_unauthorized', do: permission, requester: current_profile + # platformos-check-enable UnreachableCode + + function impersonate_user = 'modules/user/commands/session/impersonation/create', current_user_id: context.current_user.id, user: user_to_impersonate + + if impersonate_user.valid + include 'modules/core/helpers/redirect_to' + else + print "Something went wrong." + endif + else + print "Something went wrong." + endif +%} diff --git a/modules/user/public/views/pages/sessions/impersonation/destroy.liquid b/modules/user/public/views/pages/sessions/impersonation/destroy.liquid new file mode 100644 index 0000000..14d2bc9 --- /dev/null +++ b/modules/user/public/views/pages/sessions/impersonation/destroy.liquid @@ -0,0 +1,18 @@ +--- +slug: sessions/impersonations +method: delete +--- +{% liquid + if context.session.original_user_id == blank + redirect_to "/" + else + function admin_user = 'modules/user/queries/user/load', id: context.session.original_user_id + function object = 'modules/user/commands/session/impersonation/destroy', user: admin_user, current_user_id: context.current_user.id + + if object.valid + include 'modules/core/helpers/redirect_to' + else + print "Something went wrong." + endif + endif +%} diff --git a/modules/user/public/views/pages/sessions/new.liquid b/modules/user/public/views/pages/sessions/new.liquid new file mode 100644 index 0000000..d706a22 --- /dev/null +++ b/modules/user/public/views/pages/sessions/new.liquid @@ -0,0 +1,7 @@ +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + + include 'modules/user/helpers/can_do_or_redirect', requester: current_profile, do: 'sessions.create', return_url: '/' + + render 'modules/user/sessions/new', context: context, values: null +%} diff --git a/modules/user/public/views/pages/users/create.liquid b/modules/user/public/views/pages/users/create.liquid new file mode 100644 index 0000000..05889f7 --- /dev/null +++ b/modules/user/public/views/pages/users/create.liquid @@ -0,0 +1,22 @@ +--- +method: post +slug: users +--- +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + + include 'modules/user/helpers/can_do_or_redirect', requester: current_profile, do: 'users.register', redirect_url: "/" + + function object = 'modules/user/commands/user/create', first_name: params.first_name, last_name: params.last_name, email: params.email, password: params.password, hook_params: params, roles: null + if object.valid + function _ = 'modules/user/commands/session/create', user_id: object.id, validate_password: false, email: null, password: null, hook_params: null, skip_otp: null + include 'modules/core/helpers/redirect_to' + else + assign values = object | default: null | hash_merge: password: '' + + function registration_fields = 'modules/user/queries/registration_fields/load' + + assign values = params | hash_merge: password: '' + render 'modules/user/users/new', context: context, registration_fields: registration_fields, errors: object.errors, values: values + endif +%} diff --git a/modules/user/public/views/pages/users/email/edit.liquid b/modules/user/public/views/pages/users/email/edit.liquid new file mode 100644 index 0000000..577aee9 --- /dev/null +++ b/modules/user/public/views/pages/users/email/edit.liquid @@ -0,0 +1,8 @@ +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + if current_profile.user == null + include 'modules/core/helpers/redirect_to' + endif + + render 'modules/user/users/email/edit', otp_enabled: current_profile.otp_configured +%} diff --git a/modules/user/public/views/pages/users/email/update.liquid b/modules/user/public/views/pages/users/email/update.liquid new file mode 100644 index 0000000..b82cb1e --- /dev/null +++ b/modules/user/public/views/pages/users/email/update.liquid @@ -0,0 +1,32 @@ +--- +slug: users/email/edit +method: put +--- +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + if current_profile.user == null + include 'modules/core/helpers/redirect_to' + endif + + if current_profile.otp_configured + function object = 'modules/user/commands/user/verify_otp', object: context.params.user, email: current_profile.user.email + if object.valid == false + render 'modules/user/users/email/edit', otp_enabled: current_profile.otp_configured, errors: object.errors + break + endif + endif + + function object = 'modules/user/commands/user/email_update', object: context.params.user, current_user: current_profile.user, c: null + + if object.valid + hash_assign current_profile['email'] = context.params.user.email + function _ = 'modules/user/commands/profiles/update', object: current_profile, profile: current_profile + assign event_payload = null | hash_merge: actor_id: object.id, object: object, actor: object, target: null, object_id: null, target_id: null + function _event = 'modules/core/commands/events/publish', type: 'email_updated', object: event_payload, delay: null, max_attempts: null + + assign notice = 'modules/user/users.email.change_success' | t + include 'modules/core/helpers/redirect_to', url: '/', notice: notice + else + render 'modules/user/users/email/edit', context: context, otp_enabled: null + endif +%} diff --git a/modules/user/public/views/pages/users/new.liquid b/modules/user/public/views/pages/users/new.liquid new file mode 100644 index 0000000..8affc3d --- /dev/null +++ b/modules/user/public/views/pages/users/new.liquid @@ -0,0 +1,10 @@ +{% liquid + function current_profile = 'modules/user/helpers/current_profile' + + include 'modules/user/helpers/can_do_or_redirect', requester: current_profile, do: 'users.register', redirect_url: "/" + + function registration_fields = 'modules/user/queries/registration_fields/load' + assign values = null | hash_merge: email: context.params.email + + render 'modules/user/users/new', context: context, registration_fields: registration_fields, values: values +%} diff --git a/modules/user/public/views/partials/2fa/disable.liquid b/modules/user/public/views/partials/2fa/disable.liquid new file mode 100644 index 0000000..19966e8 --- /dev/null +++ b/modules/user/public/views/partials/2fa/disable.liquid @@ -0,0 +1,32 @@ +
+

+ {{ 'modules/user/2fa.disable.two_factor_authentication' | t }} +

+ +
+
+ + +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'password', errors: errors.password %} +
+ +

+ {{ 'modules/user/2fa.new.confirm_otp_code' | t }} +

+
+ + + {% render 'modules/common-styling/forms/error_list', name: 'otp_code', errors: errors.otp_code %} +
+ +
+ +
+
+
+
diff --git a/modules/user/public/views/partials/2fa/setup.liquid b/modules/user/public/views/partials/2fa/setup.liquid new file mode 100644 index 0000000..844f002 --- /dev/null +++ b/modules/user/public/views/partials/2fa/setup.liquid @@ -0,0 +1,49 @@ +
+

+ {{ 'modules/user/2fa.new.two_factor_authentication' | t }} +

+ +
+ +
+
+
+ {{ 'modules/user/2fa.new.2fa_info' | t | markdown }} +
+ +

+ {{ 'modules/user/2fa.new.scan_qr_code' | t }} +

+ +
+ {{ otp.secret_as_svg_qr_code }} +
+ +
+ {{ 'modules/user/2fa.new.if_you_cannot_scan' | t }}
+ {{ otp.secret }} +
+ + +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'otp_code', errors: errors.otp_code %} +
+ +
+ + {{ 'modules/user/2fa.new.your_password' | t }} + + {% render 'modules/common-styling/forms/error_list', name: 'password', errors: errors.password %} +
+ +
+ +
+
+
+
+
diff --git a/modules/user/public/views/partials/2fa/verify.liquid b/modules/user/public/views/partials/2fa/verify.liquid new file mode 100644 index 0000000..56b3fb2 --- /dev/null +++ b/modules/user/public/views/partials/2fa/verify.liquid @@ -0,0 +1,30 @@ +
+

+ {{ 'modules/user/2fa.new.two_factor_authentication' | t }} +

+ +
+ + + + +

+ {{ 'modules/user/2fa.new.confirm_otp_code' | t }} +

+
+ {{ 'modules/user/2fa.new.please_confirm' | t }} +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'otp_code', errors: object.errors.otp_code %} + +
+ +
+ +
+
diff --git a/modules/user/public/views/partials/admin_pages/list.liquid b/modules/user/public/views/partials/admin_pages/list.liquid new file mode 100644 index 0000000..61021c0 --- /dev/null +++ b/modules/user/public/views/partials/admin_pages/list.liquid @@ -0,0 +1,52 @@ +{% function users = 'modules/user/queries/user/get_all' %} +
+ + {% theme_render_rc 'components/atoms/heading', content: 'Users', level: 2, classes: 'pb-8' %} + + + + + {% for property in users.first %} + {% unless property[0] == 'id' or property[0] == 'hook_results' %} + + {% endunless %} + {% endfor %} + + + + + {% for user in users %} + {% liquid + assign id = user.id + assign hook_results = user.hook_results + assign user = user | hash_delete_key: 'id' + assign user = user | hash_delete_key: 'hook_results' + %} + + + {% for property in user %} + + {% endfor %} + + + {% endfor %} + +
ID{{ property[0] }}Additional fields
{{ id }} + {% assign type = property[1] | type_of %} + {% if type == 'Array' %} + {{ property[1] | join: ', ' }} + {% else %} + {{ property[1] }} + {% endif %} + + {% if hook_results %} + {% for hook_result in hook_results %} + {% if hook_result[1] %} + {{ hook_result[0] }}:
+ {{ hook_result[1] }} + {% endif %} + {% endfor %} + {% endif %} +
+ +
diff --git a/modules/user/public/views/partials/components/pages/403.liquid b/modules/user/public/views/partials/components/pages/403.liquid new file mode 100644 index 0000000..5558d8c --- /dev/null +++ b/modules/user/public/views/partials/components/pages/403.liquid @@ -0,0 +1,4 @@ +
+

403 Forbidden

+

You don't have access to this page

+
diff --git a/modules/user/public/views/partials/emails/passwords/reset.liquid b/modules/user/public/views/partials/emails/passwords/reset.liquid new file mode 100644 index 0000000..6e16d0e --- /dev/null +++ b/modules/user/public/views/partials/emails/passwords/reset.liquid @@ -0,0 +1,18 @@ +

{{ 'modules/user/emails.passwords.reset.title' | t }}

+ +

{{ 'modules/user/emails.passwords.reset.content' | t }}

+

{{ 'modules/user/emails.passwords.reset.ignore' | t }}

+ + + + + +
+ +

{{ 'modules/user/emails.passwords.reset.cta' | t }}

+ + + {{ 'modules/user/emails.passwords.reset.cta_button' | t }} + + +
diff --git a/modules/user/public/views/partials/oauth/listing.liquid b/modules/user/public/views/partials/oauth/listing.liquid new file mode 100644 index 0000000..24e3635 --- /dev/null +++ b/modules/user/public/views/partials/oauth/listing.liquid @@ -0,0 +1,48 @@ +{% liquid + function available_providers = 'modules/user/helpers/get_available_oauth_providers' + function assigned_providers = 'modules/user/helpers/get_assigned_oauth_providers' + %} + +
+

OAuth 2

+
+ + {% if available_providers.size == 0 %} + {{ 'modules/user/oauth.app.no_providers_available' | t }} + {% endif %} + +
+ {% for provider in available_providers %} + {% if assigned_providers contains provider[0] %} +
+ + + +
+ {% else %} +
+ + + +
+ {% endif %} + {% endfor %} +
\ No newline at end of file diff --git a/modules/user/public/views/partials/oauth/providers.liquid b/modules/user/public/views/partials/oauth/providers.liquid new file mode 100644 index 0000000..cb17726 --- /dev/null +++ b/modules/user/public/views/partials/oauth/providers.liquid @@ -0,0 +1,24 @@ +{% function available_providers = 'modules/user/helpers/get_available_oauth_providers' %} +{% if available_providers.size > 0 %} +
+ {{ 'modules/user/sessions.new.social_login_separator' | t }} +
+
+ {% for provider in available_providers %} +
+ + + +
+ {% endfor %} +
+{% endif %} \ No newline at end of file diff --git a/modules/user/public/views/partials/passwords/new.liquid b/modules/user/public/views/partials/passwords/new.liquid new file mode 100644 index 0000000..83a267f --- /dev/null +++ b/modules/user/public/views/partials/passwords/new.liquid @@ -0,0 +1,39 @@ +--- +metadata: + name: New password + params: + context: {} + errors: {} +--- +{% liquid + assign context = context | default: params.context + assign errors = errors | default: params.errors +%} + +
+ +

{{ 'modules/user/passwords.edit' | t }}

+ +
+ + +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'password', errors: errors['password'] %} +
+ +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'password_confirmation', errors: errors['password_confirmation'] %} +
+ +
+ +
+ +
+ +
+ diff --git a/modules/user/public/views/partials/passwords/reset.liquid b/modules/user/public/views/partials/passwords/reset.liquid new file mode 100644 index 0000000..f5f77c3 --- /dev/null +++ b/modules/user/public/views/partials/passwords/reset.liquid @@ -0,0 +1,39 @@ +--- +metadata: + name: Reset password + params: + context: {} + errors: [] +--- +{% liquid + assign context = context | default: params.context + assign errors = errors | default: params.errors +%} + +
+ +

{{ 'modules/user/passwords.reset_password_title' | t }}

+ +

{{ 'modules/user/passwords.email_desc' | t }}

+ +
+ + + +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'email', errors: errors['email'] %} +
+ + {% render 'modules/common-styling/forms/hcaptcha' %} + +
+ +
+ +
+ + {{ 'modules/user/passwords.remembered_password' | t }} {{ 'modules/user/sessions.new.log_in' | t }} + +
\ No newline at end of file diff --git a/modules/user/public/views/partials/sessions/new.liquid b/modules/user/public/views/partials/sessions/new.liquid new file mode 100644 index 0000000..27176d1 --- /dev/null +++ b/modules/user/public/views/partials/sessions/new.liquid @@ -0,0 +1,44 @@ +--- +metadata: + name: Login + params: + errors: [] +--- +{% liquid + assign errors = errors | default: params.errors +%} + +
+ +

{{ 'modules/user/sessions.new.log_in' | t }}

+ +
+ + + +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'email', errors: errors['email'] %} +
+ +
+
+ + {{ 'modules/user/passwords.forgot' | t }} +
+ + {% render 'modules/common-styling/forms/error_list', name: 'password', errors: errors['password'] %} +
+ +
+ +
+ +
+ + {{ 'modules/user/sessions.new.dont_have_account' | t }} {{ 'modules/user/sessions.new.request_to_join' | t }} + + {% render 'modules/user/oauth/providers' %} + +
\ No newline at end of file diff --git a/modules/user/public/views/partials/users/email/edit.liquid b/modules/user/public/views/partials/users/email/edit.liquid new file mode 100644 index 0000000..e42d00e --- /dev/null +++ b/modules/user/public/views/partials/users/email/edit.liquid @@ -0,0 +1,50 @@ +--- +metadata: + name: New password + params: + context: {} + errors: {} +--- +{% liquid + assign context = context | default: params.context + assign errors = errors | default: params.errors +%} + +
+ +

{{ 'modules/user/users.email.change_email' | t }}

+ +
+ + + +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'email', errors: errors['email'] %} +
+ +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'password', errors: errors['password'] %} +
+ + {% if otp_enabled %} +

+ {{ 'modules/user/2fa.new.confirm_otp_code' | t }} +

+
+ + + {% render 'modules/common-styling/forms/error_list', name: 'otp_code', errors: errors.otp_code %} +
+ {% endif %} + +
+ +
+ +
+ +
\ No newline at end of file diff --git a/modules/user/public/views/partials/users/new.liquid b/modules/user/public/views/partials/users/new.liquid new file mode 100644 index 0000000..b650700 --- /dev/null +++ b/modules/user/public/views/partials/users/new.liquid @@ -0,0 +1,73 @@ +--- +metadata: + name: Register + params: + values: {} + errors: [] + registration_fields: [] +--- + +{% liquid + assign values = values | default: params.values + assign errors = errors | default: params.errors +%} + +
+ +

{{ 'modules/user/users.new.create_account' | t }}

+ +
+ + {% for field in registration_fields %} +
+ {% if field.label %} + + {% endif %} + + {% render 'modules/common-styling/forms/error_list', name: field.name, errors: errors[field.name] %} +
+ {% endfor %} + +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'first_name', errors: errors['first_name'] %} +
+ +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'last_name', errors: errors['last_name'] %} +
+ +
+ + + {% render 'modules/common-styling/forms/error_list', name: 'email', errors: errors['email'] %} +
+ +
+ + + {% if errors['password'] %} +
    + {% for error in errors['password'] %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+ + {% render 'modules/common-styling/forms/hcaptcha' %} + +
+ +
+ +
+ + {{ 'modules/user/users.new.already_have_account' | t }} + + {% render 'modules/user/oauth/providers' %} + +
\ No newline at end of file diff --git a/modules/user/template-values.json b/modules/user/template-values.json new file mode 100644 index 0000000..3e9d9cd --- /dev/null +++ b/modules/user/template-values.json @@ -0,0 +1,10 @@ +{ + "name": "User", + "machine_name": "user", + "type": "module", + "version": "5.1.1", + "dependencies": { + "core": "^1.5.0", + "common-styling": "^1.11.0" + } +}