`,
+ * [
+ * {
+ * selector: '[expr0]',
+ * redundantAttribute: 'expr0',
+ * expressions: [
+ * {
+ * type: expressionTypes.TEXT,
+ * childNodeIndex: 0,
+ * evaluate(scope) {
+ * return scope.time;
+ * },
+ * },
+ * ],
+ * },
+ * {
+ * selector: '[expr1]',
+ * redundantAttribute: 'expr1',
+ * expressions: [
+ * {
+ * type: expressionTypes.TEXT,
+ * childNodeIndex: 0,
+ * evaluate(scope) {
+ * return scope.name;
+ * },
+ * },
+ * {
+ * type: 'attribute',
+ * name: 'style',
+ * evaluate(scope) {
+ * return scope.style;
+ * },
+ * },
+ * ],
+ * },
+ * {
+ * selector: '[expr2]',
+ * redundantAttribute: 'expr2',
+ * type: bindingTypes.IF,
+ * evaluate(scope) {
+ * return scope.isVisible;
+ * },
+ * template: riotDOMBindings.template('hello there'),
+ * },
+ * ]
+ * )
+ */
+
+var DOMBindings = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ template: create,
+ createBinding: create$1,
+ createExpression: create$4,
+ bindingTypes: bindingTypes,
+ expressionTypes: expressionTypes
+});
+
+function noop() {
+ return this;
+}
+/**
+ * Autobind the methods of a source object to itself
+ * @param {Object} source - probably a riot tag instance
+ * @param {Array
} methods - list of the methods to autobind
+ * @returns {Object} the original object received
+ */
+
+function autobindMethods(source, methods) {
+ methods.forEach(method => {
+ source[method] = source[method].bind(source);
+ });
+ return source;
+}
+/**
+ * Call the first argument received only if it's a function otherwise return it as it is
+ * @param {*} source - anything
+ * @returns {*} anything
+ */
+
+function callOrAssign(source) {
+ return isFunction(source) ? source.prototype && source.prototype.constructor ? new source() : source() : source;
+}
+
+/**
+ * Converts any DOM node/s to a loopable array
+ * @param { HTMLElement|NodeList } els - single html element or a node list
+ * @returns { Array } always a loopable object
+ */
+function domToArray(els) {
+ // can this object be already looped?
+ if (!Array.isArray(els)) {
+ // is it a node list?
+ if (/^\[object (HTMLCollection|NodeList|Object)\]$/.test(Object.prototype.toString.call(els)) && typeof els.length === 'number') return Array.from(els);else // if it's a single node
+ // it will be returned as "array" with one single entry
+ return [els];
+ } // this object could be looped out of the box
+
+
+ return els;
+}
+
+/**
+ * Simple helper to find DOM nodes returning them as array like loopable object
+ * @param { string|DOMNodeList } selector - either the query or the DOM nodes to arraify
+ * @param { HTMLElement } ctx - context defining where the query will search for the DOM nodes
+ * @returns { Array } DOM nodes found as array
+ */
+
+function $(selector, ctx) {
+ return domToArray(typeof selector === 'string' ? (ctx || document).querySelectorAll(selector) : selector);
+}
+
+/**
+ * Normalize the return values, in case of a single value we avoid to return an array
+ * @param { Array } values - list of values we want to return
+ * @returns { Array|string|boolean } either the whole list of values or the single one found
+ * @private
+ */
+
+const normalize = values => values.length === 1 ? values[0] : values;
+/**
+ * Parse all the nodes received to get/remove/check their attributes
+ * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse
+ * @param { string|Array } name - name or list of attributes
+ * @param { string } method - method that will be used to parse the attributes
+ * @returns { Array|string } result of the parsing in a list or a single value
+ * @private
+ */
+
+
+function parseNodes(els, name, method) {
+ const names = typeof name === 'string' ? [name] : name;
+ return normalize(domToArray(els).map(el => {
+ return normalize(names.map(n => el[method](n)));
+ }));
+}
+/**
+ * Set any attribute on a single or a list of DOM nodes
+ * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse
+ * @param { string|Object } name - either the name of the attribute to set
+ * or a list of properties as object key - value
+ * @param { string } value - the new value of the attribute (optional)
+ * @returns { HTMLElement|NodeList|Array } the original array of elements passed to this function
+ *
+ * @example
+ *
+ * import { set } from 'bianco.attr'
+ *
+ * const img = document.createElement('img')
+ *
+ * set(img, 'width', 100)
+ *
+ * // or also
+ * set(img, {
+ * width: 300,
+ * height: 300
+ * })
+ *
+ */
+
+
+function set(els, name, value) {
+ const attrs = typeof name === 'object' ? name : {
+ [name]: value
+ };
+ const props = Object.keys(attrs);
+ domToArray(els).forEach(el => {
+ props.forEach(prop => el.setAttribute(prop, attrs[prop]));
+ });
+ return els;
+}
+/**
+ * Get any attribute from a single or a list of DOM nodes
+ * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse
+ * @param { string|Array } name - name or list of attributes to get
+ * @returns { Array|string } list of the attributes found
+ *
+ * @example
+ *
+ * import { get } from 'bianco.attr'
+ *
+ * const img = document.createElement('img')
+ *
+ * get(img, 'width') // => '200'
+ *
+ * // or also
+ * get(img, ['width', 'height']) // => ['200', '300']
+ *
+ * // or also
+ * get([img1, img2], ['width', 'height']) // => [['200', '300'], ['500', '200']]
+ */
+
+function get(els, name) {
+ return parseNodes(els, name, 'getAttribute');
+}
+
+const CSS_BY_NAME = new Map();
+const STYLE_NODE_SELECTOR = 'style[riot]'; // memoized curried function
+
+const getStyleNode = (style => {
+ return () => {
+ // lazy evaluation:
+ // if this function was already called before
+ // we return its cached result
+ if (style) return style; // create a new style element or use an existing one
+ // and cache it internally
+
+ style = $(STYLE_NODE_SELECTOR)[0] || document.createElement('style');
+ set(style, 'type', 'text/css');
+ /* istanbul ignore next */
+
+ if (!style.parentNode) document.head.appendChild(style);
+ return style;
+ };
+})();
+/**
+ * Object that will be used to inject and manage the css of every tag instance
+ */
+
+
+var cssManager = {
+ CSS_BY_NAME,
+
+ /**
+ * Save a tag style to be later injected into DOM
+ * @param { string } name - if it's passed we will map the css to a tagname
+ * @param { string } css - css string
+ * @returns {Object} self
+ */
+ add(name, css) {
+ if (!CSS_BY_NAME.has(name)) {
+ CSS_BY_NAME.set(name, css);
+ this.inject();
+ }
+
+ return this;
+ },
+
+ /**
+ * Inject all previously saved tag styles into DOM
+ * innerHTML seems slow: http://jsperf.com/riot-insert-style
+ * @returns {Object} self
+ */
+ inject() {
+ getStyleNode().innerHTML = [...CSS_BY_NAME.values()].join('\n');
+ return this;
+ },
+
+ /**
+ * Remove a tag style from the DOM
+ * @param {string} name a registered tagname
+ * @returns {Object} self
+ */
+ remove(name) {
+ if (CSS_BY_NAME.has(name)) {
+ CSS_BY_NAME.delete(name);
+ this.inject();
+ }
+
+ return this;
+ }
+
+};
+
+/**
+ * Function to curry any javascript method
+ * @param {Function} fn - the target function we want to curry
+ * @param {...[args]} acc - initial arguments
+ * @returns {Function|*} it will return a function until the target function
+ * will receive all of its arguments
+ */
+function curry(fn) {
+ for (var _len = arguments.length, acc = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ acc[_key - 1] = arguments[_key];
+ }
+
+ return function () {
+ for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
+ args[_key2] = arguments[_key2];
+ }
+
+ args = [...acc, ...args];
+ return args.length < fn.length ? curry(fn, ...args) : fn(...args);
+ };
+}
+
+/**
+ * Get the tag name of any DOM node
+ * @param {HTMLElement} element - DOM node we want to inspect
+ * @returns {string} name to identify this dom node in riot
+ */
+
+function getName(element) {
+ return get(element, IS_DIRECTIVE) || element.tagName.toLowerCase();
+}
+
+const COMPONENT_CORE_HELPERS = Object.freeze({
+ // component helpers
+ $(selector) {
+ return $(selector, this.root)[0];
+ },
+
+ $$(selector) {
+ return $(selector, this.root);
+ }
+
+});
+const PURE_COMPONENT_API = Object.freeze({
+ [MOUNT_METHOD_KEY]: noop,
+ [UPDATE_METHOD_KEY]: noop,
+ [UNMOUNT_METHOD_KEY]: noop
+});
+const COMPONENT_LIFECYCLE_METHODS = Object.freeze({
+ [SHOULD_UPDATE_KEY]: noop,
+ [ON_BEFORE_MOUNT_KEY]: noop,
+ [ON_MOUNTED_KEY]: noop,
+ [ON_BEFORE_UPDATE_KEY]: noop,
+ [ON_UPDATED_KEY]: noop,
+ [ON_BEFORE_UNMOUNT_KEY]: noop,
+ [ON_UNMOUNTED_KEY]: noop
+});
+const MOCKED_TEMPLATE_INTERFACE = Object.assign({}, PURE_COMPONENT_API, {
+ clone: noop,
+ createDOM: noop
+});
+/**
+ * Performance optimization for the recursive components
+ * @param {RiotComponentWrapper} componentWrapper - riot compiler generated object
+ * @returns {Object} component like interface
+ */
+
+const memoizedCreateComponent = memoize(createComponent);
+/**
+ * Evaluate the component properties either from its real attributes or from its initial user properties
+ * @param {HTMLElement} element - component root
+ * @param {Object} initialProps - initial props
+ * @returns {Object} component props key value pairs
+ */
+
+function evaluateInitialProps(element, initialProps) {
+ if (initialProps === void 0) {
+ initialProps = {};
+ }
+
+ return Object.assign({}, DOMattributesToObject(element), callOrAssign(initialProps));
+}
+/**
+ * Bind a DOM node to its component object
+ * @param {HTMLElement} node - html node mounted
+ * @param {Object} component - Riot.js component object
+ * @returns {Object} the component object received as second argument
+ */
+
+
+const bindDOMNodeToComponentObject = (node, component) => node[DOM_COMPONENT_INSTANCE_PROPERTY$1] = component;
+/**
+ * Wrap the Riot.js core API methods using a mapping function
+ * @param {Function} mapFunction - lifting function
+ * @returns {Object} an object having the { mount, update, unmount } functions
+ */
+
+
+function createCoreAPIMethods(mapFunction) {
+ return [MOUNT_METHOD_KEY, UPDATE_METHOD_KEY, UNMOUNT_METHOD_KEY].reduce((acc, method) => {
+ acc[method] = mapFunction(method);
+ return acc;
+ }, {});
+}
+/**
+ * Factory function to create the component templates only once
+ * @param {Function} template - component template creation function
+ * @param {RiotComponentWrapper} componentWrapper - riot compiler generated object
+ * @returns {TemplateChunk} template chunk object
+ */
+
+
+function componentTemplateFactory(template, componentWrapper) {
+ const components = createSubcomponents(componentWrapper.exports ? componentWrapper.exports.components : {});
+ return template(create, expressionTypes, bindingTypes, name => {
+ // improve support for recursive components
+ if (name === componentWrapper.name) return memoizedCreateComponent(componentWrapper); // return the registered components
+
+ return components[name] || COMPONENTS_IMPLEMENTATION_MAP$1.get(name);
+ });
+}
+/**
+ * Create a pure component
+ * @param {Function} pureFactoryFunction - pure component factory function
+ * @param {Array} options.slots - component slots
+ * @param {Array} options.attributes - component attributes
+ * @param {Array} options.template - template factory function
+ * @param {Array} options.template - template factory function
+ * @param {any} options.props - initial component properties
+ * @returns {Object} pure component object
+ */
+
+
+function createPureComponent(pureFactoryFunction, _ref) {
+ let {
+ slots,
+ attributes,
+ props,
+ css,
+ template
+ } = _ref;
+ if (template) panic('Pure components can not have html');
+ if (css) panic('Pure components do not have css');
+ const component = defineDefaults(pureFactoryFunction({
+ slots,
+ attributes,
+ props
+ }), PURE_COMPONENT_API);
+ return createCoreAPIMethods(method => function () {
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ // intercept the mount calls to bind the DOM node to the pure object created
+ // see also https://github.com/riot/riot/issues/2806
+ if (method === MOUNT_METHOD_KEY) {
+ const [el] = args; // mark this node as pure element
+
+ el[IS_PURE_SYMBOL] = true;
+ bindDOMNodeToComponentObject(el, component);
+ }
+
+ component[method](...args);
+ return component;
+ });
+}
+/**
+ * Create the component interface needed for the @riotjs/dom-bindings tag bindings
+ * @param {RiotComponentWrapper} componentWrapper - riot compiler generated object
+ * @param {string} componentWrapper.css - component css
+ * @param {Function} componentWrapper.template - function that will return the dom-bindings template function
+ * @param {Object} componentWrapper.exports - component interface
+ * @param {string} componentWrapper.name - component name
+ * @returns {Object} component like interface
+ */
+
+
+function createComponent(componentWrapper) {
+ const {
+ css,
+ template,
+ exports,
+ name
+ } = componentWrapper;
+ const templateFn = template ? componentTemplateFactory(template, componentWrapper) : MOCKED_TEMPLATE_INTERFACE;
+ return _ref2 => {
+ let {
+ slots,
+ attributes,
+ props
+ } = _ref2;
+ // pure components rendering will be managed by the end user
+ if (exports && exports[IS_PURE_SYMBOL]) return createPureComponent(exports, {
+ slots,
+ attributes,
+ props,
+ css,
+ template
+ });
+ const componentAPI = callOrAssign(exports) || {};
+ const component = defineComponent({
+ css,
+ template: templateFn,
+ componentAPI,
+ name
+ })({
+ slots,
+ attributes,
+ props
+ }); // notice that for the components create via tag binding
+ // we need to invert the mount (state/parentScope) arguments
+ // the template bindings will only forward the parentScope updates
+ // and never deal with the component state
+
+ return {
+ mount(element, parentScope, state) {
+ return component.mount(element, state, parentScope);
+ },
+
+ update(parentScope, state) {
+ return component.update(state, parentScope);
+ },
+
+ unmount(preserveRoot) {
+ return component.unmount(preserveRoot);
+ }
+
+ };
+ };
+}
+/**
+ * Component definition function
+ * @param {Object} implementation - the componen implementation will be generated via compiler
+ * @param {Object} component - the component initial properties
+ * @returns {Object} a new component implementation object
+ */
+
+function defineComponent(_ref3) {
+ let {
+ css,
+ template,
+ componentAPI,
+ name
+ } = _ref3;
+ // add the component css into the DOM
+ if (css && name) cssManager.add(name, css);
+ return curry(enhanceComponentAPI)(defineProperties( // set the component defaults without overriding the original component API
+ defineDefaults(componentAPI, Object.assign({}, COMPONENT_LIFECYCLE_METHODS, {
+ [PROPS_KEY]: {},
+ [STATE_KEY]: {}
+ })), Object.assign({
+ // defined during the component creation
+ [SLOTS_KEY]: null,
+ [ROOT_KEY]: null
+ }, COMPONENT_CORE_HELPERS, {
+ name,
+ css,
+ template
+ })));
+}
+/**
+ * Create the bindings to update the component attributes
+ * @param {HTMLElement} node - node where we will bind the expressions
+ * @param {Array} attributes - list of attribute bindings
+ * @returns {TemplateChunk} - template bindings object
+ */
+
+function createAttributeBindings(node, attributes) {
+ if (attributes === void 0) {
+ attributes = [];
+ }
+
+ const expressions = attributes.map(a => create$4(node, a));
+ const binding = {};
+ return Object.assign(binding, Object.assign({
+ expressions
+ }, createCoreAPIMethods(method => scope => {
+ expressions.forEach(e => e[method](scope));
+ return binding;
+ })));
+}
+/**
+ * Create the subcomponents that can be included inside a tag in runtime
+ * @param {Object} components - components imported in runtime
+ * @returns {Object} all the components transformed into Riot.Component factory functions
+ */
+
+
+function createSubcomponents(components) {
+ if (components === void 0) {
+ components = {};
+ }
+
+ return Object.entries(callOrAssign(components)).reduce((acc, _ref4) => {
+ let [key, value] = _ref4;
+ acc[camelToDashCase(key)] = createComponent(value);
+ return acc;
+ }, {});
+}
+/**
+ * Run the component instance through all the plugins set by the user
+ * @param {Object} component - component instance
+ * @returns {Object} the component enhanced by the plugins
+ */
+
+
+function runPlugins(component) {
+ return [...PLUGINS_SET$1].reduce((c, fn) => fn(c) || c, component);
+}
+/**
+ * Compute the component current state merging it with its previous state
+ * @param {Object} oldState - previous state object
+ * @param {Object} newState - new state givent to the `update` call
+ * @returns {Object} new object state
+ */
+
+
+function computeState(oldState, newState) {
+ return Object.assign({}, oldState, callOrAssign(newState));
+}
+/**
+ * Add eventually the "is" attribute to link this DOM node to its css
+ * @param {HTMLElement} element - target root node
+ * @param {string} name - name of the component mounted
+ * @returns {undefined} it's a void function
+ */
+
+
+function addCssHook(element, name) {
+ if (getName(element) !== name) {
+ set(element, IS_DIRECTIVE, name);
+ }
+}
+/**
+ * Component creation factory function that will enhance the user provided API
+ * @param {Object} component - a component implementation previously defined
+ * @param {Array} options.slots - component slots generated via riot compiler
+ * @param {Array} options.attributes - attribute expressions generated via riot compiler
+ * @returns {Riot.Component} a riot component instance
+ */
+
+
+function enhanceComponentAPI(component, _ref5) {
+ let {
+ slots,
+ attributes,
+ props
+ } = _ref5;
+ return autobindMethods(runPlugins(defineProperties(isObject(component) ? Object.create(component) : component, {
+ mount(element, state, parentScope) {
+ if (state === void 0) {
+ state = {};
+ }
+
+ this[PARENT_KEY_SYMBOL] = parentScope;
+ this[ATTRIBUTES_KEY_SYMBOL] = createAttributeBindings(element, attributes).mount(parentScope);
+ defineProperty(this, PROPS_KEY, Object.freeze(Object.assign({}, evaluateInitialProps(element, props), evaluateAttributeExpressions(this[ATTRIBUTES_KEY_SYMBOL].expressions))));
+ this[STATE_KEY] = computeState(this[STATE_KEY], state);
+ this[TEMPLATE_KEY_SYMBOL] = this.template.createDOM(element).clone(); // link this object to the DOM node
+
+ bindDOMNodeToComponentObject(element, this); // add eventually the 'is' attribute
+
+ component.name && addCssHook(element, component.name); // define the root element
+
+ defineProperty(this, ROOT_KEY, element); // define the slots array
+
+ defineProperty(this, SLOTS_KEY, slots); // before mount lifecycle event
+
+ this[ON_BEFORE_MOUNT_KEY](this[PROPS_KEY], this[STATE_KEY]); // mount the template
+
+ this[TEMPLATE_KEY_SYMBOL].mount(element, this, parentScope);
+ this[ON_MOUNTED_KEY](this[PROPS_KEY], this[STATE_KEY]);
+ return this;
+ },
+
+ update(state, parentScope) {
+ if (state === void 0) {
+ state = {};
+ }
+
+ if (parentScope) {
+ this[PARENT_KEY_SYMBOL] = parentScope;
+ this[ATTRIBUTES_KEY_SYMBOL].update(parentScope);
+ }
+
+ const newProps = evaluateAttributeExpressions(this[ATTRIBUTES_KEY_SYMBOL].expressions);
+ if (this[SHOULD_UPDATE_KEY](newProps, this[PROPS_KEY]) === false) return;
+ defineProperty(this, PROPS_KEY, Object.freeze(Object.assign({}, this[PROPS_KEY], newProps)));
+ this[STATE_KEY] = computeState(this[STATE_KEY], state);
+ this[ON_BEFORE_UPDATE_KEY](this[PROPS_KEY], this[STATE_KEY]); // avoiding recursive updates
+ // see also https://github.com/riot/riot/issues/2895
+
+ if (!this[IS_COMPONENT_UPDATING]) {
+ this[IS_COMPONENT_UPDATING] = true;
+ this[TEMPLATE_KEY_SYMBOL].update(this, this[PARENT_KEY_SYMBOL]);
+ }
+
+ this[ON_UPDATED_KEY](this[PROPS_KEY], this[STATE_KEY]);
+ this[IS_COMPONENT_UPDATING] = false;
+ return this;
+ },
+
+ unmount(preserveRoot) {
+ this[ON_BEFORE_UNMOUNT_KEY](this[PROPS_KEY], this[STATE_KEY]);
+ this[ATTRIBUTES_KEY_SYMBOL].unmount(); // if the preserveRoot is null the template html will be left untouched
+ // in that case the DOM cleanup will happen differently from a parent node
+
+ this[TEMPLATE_KEY_SYMBOL].unmount(this, this[PARENT_KEY_SYMBOL], preserveRoot === null ? null : !preserveRoot);
+ this[ON_UNMOUNTED_KEY](this[PROPS_KEY], this[STATE_KEY]);
+ return this;
+ }
+
+ })), Object.keys(component).filter(prop => isFunction(component[prop])));
+}
+/**
+ * Component initialization function starting from a DOM node
+ * @param {HTMLElement} element - element to upgrade
+ * @param {Object} initialProps - initial component properties
+ * @param {string} componentName - component id
+ * @returns {Object} a new component instance bound to a DOM node
+ */
+
+function mountComponent(element, initialProps, componentName) {
+ const name = componentName || getName(element);
+ if (!COMPONENTS_IMPLEMENTATION_MAP$1.has(name)) panic(`The component named "${name}" was never registered`);
+ const component = COMPONENTS_IMPLEMENTATION_MAP$1.get(name)({
+ props: initialProps
+ });
+ return component.mount(element);
+}
+
+/**
+ * Similar to compose but performs from left-to-right function composition.
+ * {@link https://30secondsofcode.org/function#composeright see also}
+ * @param {...[function]} fns) - list of unary function
+ * @returns {*} result of the computation
+ */
+/**
+ * Performs right-to-left function composition.
+ * Use Array.prototype.reduce() to perform right-to-left function composition.
+ * The last (rightmost) function can accept one or more arguments; the remaining functions must be unary.
+ * {@link https://30secondsofcode.org/function#compose original source code}
+ * @param {...[function]} fns) - list of unary function
+ * @returns {*} result of the computation
+ */
+
+function compose() {
+ for (var _len2 = arguments.length, fns = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
+ fns[_key2] = arguments[_key2];
+ }
+
+ return fns.reduce((f, g) => function () {
+ return f(g(...arguments));
+ });
+}
+
+const {
+ DOM_COMPONENT_INSTANCE_PROPERTY,
+ COMPONENTS_IMPLEMENTATION_MAP,
+ PLUGINS_SET
+} = globals;
+/**
+ * Riot public api
+ */
+
+/**
+ * Register a custom tag by name
+ * @param {string} name - component name
+ * @param {Object} implementation - tag implementation
+ * @returns {Map} map containing all the components implementations
+ */
+
+function register(name, _ref) {
+ let {
+ css,
+ template,
+ exports
+ } = _ref;
+ if (COMPONENTS_IMPLEMENTATION_MAP.has(name)) panic(`The component "${name}" was already registered`);
+ COMPONENTS_IMPLEMENTATION_MAP.set(name, createComponent({
+ name,
+ css,
+ template,
+ exports
+ }));
+ return COMPONENTS_IMPLEMENTATION_MAP;
+}
+/**
+ * Unregister a riot web component
+ * @param {string} name - component name
+ * @returns {Map} map containing all the components implementations
+ */
+
+function unregister(name) {
+ if (!COMPONENTS_IMPLEMENTATION_MAP.has(name)) panic(`The component "${name}" was never registered`);
+ COMPONENTS_IMPLEMENTATION_MAP.delete(name);
+ cssManager.remove(name);
+ return COMPONENTS_IMPLEMENTATION_MAP;
+}
+/**
+ * Mounting function that will work only for the components that were globally registered
+ * @param {string|HTMLElement} selector - query for the selection or a DOM element
+ * @param {Object} initialProps - the initial component properties
+ * @param {string} name - optional component name
+ * @returns {Array} list of riot components
+ */
+
+function mount(selector, initialProps, name) {
+ return $(selector).map(element => mountComponent(element, initialProps, name));
+}
+/**
+ * Sweet unmounting helper function for the DOM node mounted manually by the user
+ * @param {string|HTMLElement} selector - query for the selection or a DOM element
+ * @param {boolean|null} keepRootElement - if true keep the root element
+ * @returns {Array} list of nodes unmounted
+ */
+
+function unmount(selector, keepRootElement) {
+ return $(selector).map(element => {
+ if (element[DOM_COMPONENT_INSTANCE_PROPERTY]) {
+ element[DOM_COMPONENT_INSTANCE_PROPERTY].unmount(keepRootElement);
+ }
+
+ return element;
+ });
+}
+/**
+ * Define a riot plugin
+ * @param {Function} plugin - function that will receive all the components created
+ * @returns {Set} the set containing all the plugins installed
+ */
+
+function install(plugin) {
+ if (!isFunction(plugin)) panic('Plugins must be of type function');
+ if (PLUGINS_SET.has(plugin)) panic('This plugin was already installed');
+ PLUGINS_SET.add(plugin);
+ return PLUGINS_SET;
+}
+/**
+ * Uninstall a riot plugin
+ * @param {Function} plugin - plugin previously installed
+ * @returns {Set} the set containing all the plugins installed
+ */
+
+function uninstall(plugin) {
+ if (!PLUGINS_SET.has(plugin)) panic('This plugin was never installed');
+ PLUGINS_SET.delete(plugin);
+ return PLUGINS_SET;
+}
+/**
+ * Helper method to create component without relying on the registered ones
+ * @param {Object} implementation - component implementation
+ * @returns {Function} function that will allow you to mount a riot component on a DOM node
+ */
+
+function component(implementation) {
+ return function (el, props, _temp) {
+ let {
+ slots,
+ attributes,
+ parentScope
+ } = _temp === void 0 ? {} : _temp;
+ return compose(c => c.mount(el, parentScope), c => c({
+ props,
+ slots,
+ attributes
+ }), createComponent)(implementation);
+ };
+}
+/**
+ * Lift a riot component Interface into a pure riot object
+ * @param {Function} func - RiotPureComponent factory function
+ * @returns {Function} the lifted original function received as argument
+ */
+
+function pure(func) {
+ if (!isFunction(func)) panic('riot.pure accepts only arguments of type "function"');
+ func[IS_PURE_SYMBOL] = true;
+ return func;
+}
+/**
+ * no-op function needed to add the proper types to your component via typescript
+ * @param {Function|Object} component - component default export
+ * @returns {Function|Object} returns exactly what it has received
+ */
+
+const withTypes = component => component;
+/** @type {string} current riot version */
+
+const version = 'v6.0.1'; // expose some internal stuff that might be used from external tools
+
+const __ = {
+ cssManager,
+ DOMBindings,
+ createComponent,
+ defineComponent,
+ globals
+};
+
+
+
+
+/***/ }),
+
+/***/ "./node_modules/validate.js/validate.js":
+/*!**********************************************!*\
+ !*** ./node_modules/validate.js/validate.js ***!
+ \**********************************************/
+/***/ (function(module, exports, __webpack_require__) {
+
+/* module decorator */ module = __webpack_require__.nmd(module);
+/*!
+ * validate.js 0.13.1
+ *
+ * (c) 2013-2019 Nicklas Ansman, 2013 Wrapp
+ * Validate.js may be freely distributed under the MIT license.
+ * For all details and documentation:
+ * http://validatejs.org/
+ */
+
+(function(exports, module, define) {
+ "use strict";
+
+ // The main function that calls the validators specified by the constraints.
+ // The options are the following:
+ // - format (string) - An option that controls how the returned value is formatted
+ // * flat - Returns a flat array of just the error messages
+ // * grouped - Returns the messages grouped by attribute (default)
+ // * detailed - Returns an array of the raw validation data
+ // - fullMessages (boolean) - If `true` (default) the attribute name is prepended to the error.
+ //
+ // Please note that the options are also passed to each validator.
+ var validate = function(attributes, constraints, options) {
+ options = v.extend({}, v.options, options);
+
+ var results = v.runValidations(attributes, constraints, options)
+ , attr
+ , validator;
+
+ if (results.some(function(r) { return v.isPromise(r.error); })) {
+ throw new Error("Use validate.async if you want support for promises");
+ }
+ return validate.processValidationResults(results, options);
+ };
+
+ var v = validate;
+
+ // Copies over attributes from one or more sources to a single destination.
+ // Very much similar to underscore's extend.
+ // The first argument is the target object and the remaining arguments will be
+ // used as sources.
+ v.extend = function(obj) {
+ [].slice.call(arguments, 1).forEach(function(source) {
+ for (var attr in source) {
+ obj[attr] = source[attr];
+ }
+ });
+ return obj;
+ };
+
+ v.extend(validate, {
+ // This is the version of the library as a semver.
+ // The toString function will allow it to be coerced into a string
+ version: {
+ major: 0,
+ minor: 13,
+ patch: 1,
+ metadata: null,
+ toString: function() {
+ var version = v.format("%{major}.%{minor}.%{patch}", v.version);
+ if (!v.isEmpty(v.version.metadata)) {
+ version += "+" + v.version.metadata;
+ }
+ return version;
+ }
+ },
+
+ // Below is the dependencies that are used in validate.js
+
+ // The constructor of the Promise implementation.
+ // If you are using Q.js, RSVP or any other A+ compatible implementation
+ // override this attribute to be the constructor of that promise.
+ // Since jQuery promises aren't A+ compatible they won't work.
+ Promise: typeof Promise !== "undefined" ? Promise : /* istanbul ignore next */ null,
+
+ EMPTY_STRING_REGEXP: /^\s*$/,
+
+ // Runs the validators specified by the constraints object.
+ // Will return an array of the format:
+ // [{attribute: "", error: ""}, ...]
+ runValidations: function(attributes, constraints, options) {
+ var results = []
+ , attr
+ , validatorName
+ , value
+ , validators
+ , validator
+ , validatorOptions
+ , error;
+
+ if (v.isDomElement(attributes) || v.isJqueryElement(attributes)) {
+ attributes = v.collectFormValues(attributes);
+ }
+
+ // Loops through each constraints, finds the correct validator and run it.
+ for (attr in constraints) {
+ value = v.getDeepObjectValue(attributes, attr);
+ // This allows the constraints for an attribute to be a function.
+ // The function will be called with the value, attribute name, the complete dict of
+ // attributes as well as the options and constraints passed in.
+ // This is useful when you want to have different
+ // validations depending on the attribute value.
+ validators = v.result(constraints[attr], value, attributes, attr, options, constraints);
+
+ for (validatorName in validators) {
+ validator = v.validators[validatorName];
+
+ if (!validator) {
+ error = v.format("Unknown validator %{name}", {name: validatorName});
+ throw new Error(error);
+ }
+
+ validatorOptions = validators[validatorName];
+ // This allows the options to be a function. The function will be
+ // called with the value, attribute name, the complete dict of
+ // attributes as well as the options and constraints passed in.
+ // This is useful when you want to have different
+ // validations depending on the attribute value.
+ validatorOptions = v.result(validatorOptions, value, attributes, attr, options, constraints);
+ if (!validatorOptions) {
+ continue;
+ }
+ results.push({
+ attribute: attr,
+ value: value,
+ validator: validatorName,
+ globalOptions: options,
+ attributes: attributes,
+ options: validatorOptions,
+ error: validator.call(validator,
+ value,
+ validatorOptions,
+ attr,
+ attributes,
+ options)
+ });
+ }
+ }
+
+ return results;
+ },
+
+ // Takes the output from runValidations and converts it to the correct
+ // output format.
+ processValidationResults: function(errors, options) {
+ errors = v.pruneEmptyErrors(errors, options);
+ errors = v.expandMultipleErrors(errors, options);
+ errors = v.convertErrorMessages(errors, options);
+
+ var format = options.format || "grouped";
+
+ if (typeof v.formatters[format] === 'function') {
+ errors = v.formatters[format](errors);
+ } else {
+ throw new Error(v.format("Unknown format %{format}", options));
+ }
+
+ return v.isEmpty(errors) ? undefined : errors;
+ },
+
+ // Runs the validations with support for promises.
+ // This function will return a promise that is settled when all the
+ // validation promises have been completed.
+ // It can be called even if no validations returned a promise.
+ async: function(attributes, constraints, options) {
+ options = v.extend({}, v.async.options, options);
+
+ var WrapErrors = options.wrapErrors || function(errors) {
+ return errors;
+ };
+
+ // Removes unknown attributes
+ if (options.cleanAttributes !== false) {
+ attributes = v.cleanAttributes(attributes, constraints);
+ }
+
+ var results = v.runValidations(attributes, constraints, options);
+
+ return new v.Promise(function(resolve, reject) {
+ v.waitForResults(results).then(function() {
+ var errors = v.processValidationResults(results, options);
+ if (errors) {
+ reject(new WrapErrors(errors, options, attributes, constraints));
+ } else {
+ resolve(attributes);
+ }
+ }, function(err) {
+ reject(err);
+ });
+ });
+ },
+
+ single: function(value, constraints, options) {
+ options = v.extend({}, v.single.options, options, {
+ format: "flat",
+ fullMessages: false
+ });
+ return v({single: value}, {single: constraints}, options);
+ },
+
+ // Returns a promise that is resolved when all promises in the results array
+ // are settled. The promise returned from this function is always resolved,
+ // never rejected.
+ // This function modifies the input argument, it replaces the promises
+ // with the value returned from the promise.
+ waitForResults: function(results) {
+ // Create a sequence of all the results starting with a resolved promise.
+ return results.reduce(function(memo, result) {
+ // If this result isn't a promise skip it in the sequence.
+ if (!v.isPromise(result.error)) {
+ return memo;
+ }
+
+ return memo.then(function() {
+ return result.error.then(function(error) {
+ result.error = error || null;
+ });
+ });
+ }, new v.Promise(function(r) { r(); })); // A resolved promise
+ },
+
+ // If the given argument is a call: function the and: function return the value
+ // otherwise just return the value. Additional arguments will be passed as
+ // arguments to the function.
+ // Example:
+ // ```
+ // result('foo') // 'foo'
+ // result(Math.max, 1, 2) // 2
+ // ```
+ result: function(value) {
+ var args = [].slice.call(arguments, 1);
+ if (typeof value === 'function') {
+ value = value.apply(null, args);
+ }
+ return value;
+ },
+
+ // Checks if the value is a number. This function does not consider NaN a
+ // number like many other `isNumber` functions do.
+ isNumber: function(value) {
+ return typeof value === 'number' && !isNaN(value);
+ },
+
+ // Returns false if the object is not a function
+ isFunction: function(value) {
+ return typeof value === 'function';
+ },
+
+ // A simple check to verify that the value is an integer. Uses `isNumber`
+ // and a simple modulo check.
+ isInteger: function(value) {
+ return v.isNumber(value) && value % 1 === 0;
+ },
+
+ // Checks if the value is a boolean
+ isBoolean: function(value) {
+ return typeof value === 'boolean';
+ },
+
+ // Uses the `Object` function to check if the given argument is an object.
+ isObject: function(obj) {
+ return obj === Object(obj);
+ },
+
+ // Simply checks if the object is an instance of a date
+ isDate: function(obj) {
+ return obj instanceof Date;
+ },
+
+ // Returns false if the object is `null` of `undefined`
+ isDefined: function(obj) {
+ return obj !== null && obj !== undefined;
+ },
+
+ // Checks if the given argument is a promise. Anything with a `then`
+ // function is considered a promise.
+ isPromise: function(p) {
+ return !!p && v.isFunction(p.then);
+ },
+
+ isJqueryElement: function(o) {
+ return o && v.isString(o.jquery);
+ },
+
+ isDomElement: function(o) {
+ if (!o) {
+ return false;
+ }
+
+ if (!o.querySelectorAll || !o.querySelector) {
+ return false;
+ }
+
+ if (v.isObject(document) && o === document) {
+ return true;
+ }
+
+ // http://stackoverflow.com/a/384380/699304
+ /* istanbul ignore else */
+ if (typeof HTMLElement === "object") {
+ return o instanceof HTMLElement;
+ } else {
+ return o &&
+ typeof o === "object" &&
+ o !== null &&
+ o.nodeType === 1 &&
+ typeof o.nodeName === "string";
+ }
+ },
+
+ isEmpty: function(value) {
+ var attr;
+
+ // Null and undefined are empty
+ if (!v.isDefined(value)) {
+ return true;
+ }
+
+ // functions are non empty
+ if (v.isFunction(value)) {
+ return false;
+ }
+
+ // Whitespace only strings are empty
+ if (v.isString(value)) {
+ return v.EMPTY_STRING_REGEXP.test(value);
+ }
+
+ // For arrays we use the length property
+ if (v.isArray(value)) {
+ return value.length === 0;
+ }
+
+ // Dates have no attributes but aren't empty
+ if (v.isDate(value)) {
+ return false;
+ }
+
+ // If we find at least one property we consider it non empty
+ if (v.isObject(value)) {
+ for (attr in value) {
+ return false;
+ }
+ return true;
+ }
+
+ return false;
+ },
+
+ // Formats the specified strings with the given values like so:
+ // ```
+ // format("Foo: %{foo}", {foo: "bar"}) // "Foo bar"
+ // ```
+ // If you want to write %{...} without having it replaced simply
+ // prefix it with % like this `Foo: %%{foo}` and it will be returned
+ // as `"Foo: %{foo}"`
+ format: v.extend(function(str, vals) {
+ if (!v.isString(str)) {
+ return str;
+ }
+ return str.replace(v.format.FORMAT_REGEXP, function(m0, m1, m2) {
+ if (m1 === '%') {
+ return "%{" + m2 + "}";
+ } else {
+ return String(vals[m2]);
+ }
+ });
+ }, {
+ // Finds %{key} style patterns in the given string
+ FORMAT_REGEXP: /(%?)%\{([^\}]+)\}/g
+ }),
+
+ // "Prettifies" the given string.
+ // Prettifying means replacing [.\_-] with spaces as well as splitting
+ // camel case words.
+ prettify: function(str) {
+ if (v.isNumber(str)) {
+ // If there are more than 2 decimals round it to two
+ if ((str * 100) % 1 === 0) {
+ return "" + str;
+ } else {
+ return parseFloat(Math.round(str * 100) / 100).toFixed(2);
+ }
+ }
+
+ if (v.isArray(str)) {
+ return str.map(function(s) { return v.prettify(s); }).join(", ");
+ }
+
+ if (v.isObject(str)) {
+ if (!v.isDefined(str.toString)) {
+ return JSON.stringify(str);
+ }
+
+ return str.toString();
+ }
+
+ // Ensure the string is actually a string
+ str = "" + str;
+
+ return str
+ // Splits keys separated by periods
+ .replace(/([^\s])\.([^\s])/g, '$1 $2')
+ // Removes backslashes
+ .replace(/\\+/g, '')
+ // Replaces - and - with space
+ .replace(/[_-]/g, ' ')
+ // Splits camel cased words
+ .replace(/([a-z])([A-Z])/g, function(m0, m1, m2) {
+ return "" + m1 + " " + m2.toLowerCase();
+ })
+ .toLowerCase();
+ },
+
+ stringifyValue: function(value, options) {
+ var prettify = options && options.prettify || v.prettify;
+ return prettify(value);
+ },
+
+ isString: function(value) {
+ return typeof value === 'string';
+ },
+
+ isArray: function(value) {
+ return {}.toString.call(value) === '[object Array]';
+ },
+
+ // Checks if the object is a hash, which is equivalent to an object that
+ // is neither an array nor a function.
+ isHash: function(value) {
+ return v.isObject(value) && !v.isArray(value) && !v.isFunction(value);
+ },
+
+ contains: function(obj, value) {
+ if (!v.isDefined(obj)) {
+ return false;
+ }
+ if (v.isArray(obj)) {
+ return obj.indexOf(value) !== -1;
+ }
+ return value in obj;
+ },
+
+ unique: function(array) {
+ if (!v.isArray(array)) {
+ return array;
+ }
+ return array.filter(function(el, index, array) {
+ return array.indexOf(el) == index;
+ });
+ },
+
+ forEachKeyInKeypath: function(object, keypath, callback) {
+ if (!v.isString(keypath)) {
+ return undefined;
+ }
+
+ var key = ""
+ , i
+ , escape = false;
+
+ for (i = 0; i < keypath.length; ++i) {
+ switch (keypath[i]) {
+ case '.':
+ if (escape) {
+ escape = false;
+ key += '.';
+ } else {
+ object = callback(object, key, false);
+ key = "";
+ }
+ break;
+
+ case '\\':
+ if (escape) {
+ escape = false;
+ key += '\\';
+ } else {
+ escape = true;
+ }
+ break;
+
+ default:
+ escape = false;
+ key += keypath[i];
+ break;
+ }
+ }
+
+ return callback(object, key, true);
+ },
+
+ getDeepObjectValue: function(obj, keypath) {
+ if (!v.isObject(obj)) {
+ return undefined;
+ }
+
+ return v.forEachKeyInKeypath(obj, keypath, function(obj, key) {
+ if (v.isObject(obj)) {
+ return obj[key];
+ }
+ });
+ },
+
+ // This returns an object with all the values of the form.
+ // It uses the input name as key and the value as value
+ // So for example this:
+ //
+ // would return:
+ // {email: "foo@bar.com"}
+ collectFormValues: function(form, options) {
+ var values = {}
+ , i
+ , j
+ , input
+ , inputs
+ , option
+ , value;
+
+ if (v.isJqueryElement(form)) {
+ form = form[0];
+ }
+
+ if (!form) {
+ return values;
+ }
+
+ options = options || {};
+
+ inputs = form.querySelectorAll("input[name], textarea[name]");
+ for (i = 0; i < inputs.length; ++i) {
+ input = inputs.item(i);
+
+ if (v.isDefined(input.getAttribute("data-ignored"))) {
+ continue;
+ }
+
+ var name = input.name.replace(/\./g, "\\\\.");
+ value = v.sanitizeFormValue(input.value, options);
+ if (input.type === "number") {
+ value = value ? +value : null;
+ } else if (input.type === "checkbox") {
+ if (input.attributes.value) {
+ if (!input.checked) {
+ value = values[name] || null;
+ }
+ } else {
+ value = input.checked;
+ }
+ } else if (input.type === "radio") {
+ if (!input.checked) {
+ value = values[name] || null;
+ }
+ }
+ values[name] = value;
+ }
+
+ inputs = form.querySelectorAll("select[name]");
+ for (i = 0; i < inputs.length; ++i) {
+ input = inputs.item(i);
+ if (v.isDefined(input.getAttribute("data-ignored"))) {
+ continue;
+ }
+
+ if (input.multiple) {
+ value = [];
+ for (j in input.options) {
+ option = input.options[j];
+ if (option && option.selected) {
+ value.push(v.sanitizeFormValue(option.value, options));
+ }
+ }
+ } else {
+ var _val = typeof input.options[input.selectedIndex] !== 'undefined' ? input.options[input.selectedIndex].value : /* istanbul ignore next */ '';
+ value = v.sanitizeFormValue(_val, options);
+ }
+ values[input.name] = value;
+ }
+
+ return values;
+ },
+
+ sanitizeFormValue: function(value, options) {
+ if (options.trim && v.isString(value)) {
+ value = value.trim();
+ }
+
+ if (options.nullify !== false && value === "") {
+ return null;
+ }
+ return value;
+ },
+
+ capitalize: function(str) {
+ if (!v.isString(str)) {
+ return str;
+ }
+ return str[0].toUpperCase() + str.slice(1);
+ },
+
+ // Remove all errors who's error attribute is empty (null or undefined)
+ pruneEmptyErrors: function(errors) {
+ return errors.filter(function(error) {
+ return !v.isEmpty(error.error);
+ });
+ },
+
+ // In
+ // [{error: ["err1", "err2"], ...}]
+ // Out
+ // [{error: "err1", ...}, {error: "err2", ...}]
+ //
+ // All attributes in an error with multiple messages are duplicated
+ // when expanding the errors.
+ expandMultipleErrors: function(errors) {
+ var ret = [];
+ errors.forEach(function(error) {
+ // Removes errors without a message
+ if (v.isArray(error.error)) {
+ error.error.forEach(function(msg) {
+ ret.push(v.extend({}, error, {error: msg}));
+ });
+ } else {
+ ret.push(error);
+ }
+ });
+ return ret;
+ },
+
+ // Converts the error mesages by prepending the attribute name unless the
+ // message is prefixed by ^
+ convertErrorMessages: function(errors, options) {
+ options = options || {};
+
+ var ret = []
+ , prettify = options.prettify || v.prettify;
+ errors.forEach(function(errorInfo) {
+ var error = v.result(errorInfo.error,
+ errorInfo.value,
+ errorInfo.attribute,
+ errorInfo.options,
+ errorInfo.attributes,
+ errorInfo.globalOptions);
+
+ if (!v.isString(error)) {
+ ret.push(errorInfo);
+ return;
+ }
+
+ if (error[0] === '^') {
+ error = error.slice(1);
+ } else if (options.fullMessages !== false) {
+ error = v.capitalize(prettify(errorInfo.attribute)) + " " + error;
+ }
+ error = error.replace(/\\\^/g, "^");
+ error = v.format(error, {
+ value: v.stringifyValue(errorInfo.value, options)
+ });
+ ret.push(v.extend({}, errorInfo, {error: error}));
+ });
+ return ret;
+ },
+
+ // In:
+ // [{attribute: "", ...}]
+ // Out:
+ // {"": [{attribute: "", ...}]}
+ groupErrorsByAttribute: function(errors) {
+ var ret = {};
+ errors.forEach(function(error) {
+ var list = ret[error.attribute];
+ if (list) {
+ list.push(error);
+ } else {
+ ret[error.attribute] = [error];
+ }
+ });
+ return ret;
+ },
+
+ // In:
+ // [{error: "", ...}, {error: "", ...}]
+ // Out:
+ // ["", ""]
+ flattenErrorsToArray: function(errors) {
+ return errors
+ .map(function(error) { return error.error; })
+ .filter(function(value, index, self) {
+ return self.indexOf(value) === index;
+ });
+ },
+
+ cleanAttributes: function(attributes, whitelist) {
+ function whitelistCreator(obj, key, last) {
+ if (v.isObject(obj[key])) {
+ return obj[key];
+ }
+ return (obj[key] = last ? true : {});
+ }
+
+ function buildObjectWhitelist(whitelist) {
+ var ow = {}
+ , lastObject
+ , attr;
+ for (attr in whitelist) {
+ if (!whitelist[attr]) {
+ continue;
+ }
+ v.forEachKeyInKeypath(ow, attr, whitelistCreator);
+ }
+ return ow;
+ }
+
+ function cleanRecursive(attributes, whitelist) {
+ if (!v.isObject(attributes)) {
+ return attributes;
+ }
+
+ var ret = v.extend({}, attributes)
+ , w
+ , attribute;
+
+ for (attribute in attributes) {
+ w = whitelist[attribute];
+
+ if (v.isObject(w)) {
+ ret[attribute] = cleanRecursive(ret[attribute], w);
+ } else if (!w) {
+ delete ret[attribute];
+ }
+ }
+ return ret;
+ }
+
+ if (!v.isObject(whitelist) || !v.isObject(attributes)) {
+ return {};
+ }
+
+ whitelist = buildObjectWhitelist(whitelist);
+ return cleanRecursive(attributes, whitelist);
+ },
+
+ exposeModule: function(validate, root, exports, module, define) {
+ if (exports) {
+ if (module && module.exports) {
+ exports = module.exports = validate;
+ }
+ exports.validate = validate;
+ } else {
+ root.validate = validate;
+ if (validate.isFunction(define) && define.amd) {
+ define([], function () { return validate; });
+ }
+ }
+ },
+
+ warn: function(msg) {
+ if (typeof console !== "undefined" && console.warn) {
+ console.warn("[validate.js] " + msg);
+ }
+ },
+
+ error: function(msg) {
+ if (typeof console !== "undefined" && console.error) {
+ console.error("[validate.js] " + msg);
+ }
+ }
+ });
+
+ validate.validators = {
+ // Presence validates that the value isn't empty
+ presence: function(value, options) {
+ options = v.extend({}, this.options, options);
+ if (options.allowEmpty !== false ? !v.isDefined(value) : v.isEmpty(value)) {
+ return options.message || this.message || "can't be blank";
+ }
+ },
+ length: function(value, options, attribute) {
+ // Empty values are allowed
+ if (!v.isDefined(value)) {
+ return;
+ }
+
+ options = v.extend({}, this.options, options);
+
+ var is = options.is
+ , maximum = options.maximum
+ , minimum = options.minimum
+ , tokenizer = options.tokenizer || function(val) { return val; }
+ , err
+ , errors = [];
+
+ value = tokenizer(value);
+ var length = value.length;
+ if(!v.isNumber(length)) {
+ return options.message || this.notValid || "has an incorrect length";
+ }
+
+ // Is checks
+ if (v.isNumber(is) && length !== is) {
+ err = options.wrongLength ||
+ this.wrongLength ||
+ "is the wrong length (should be %{count} characters)";
+ errors.push(v.format(err, {count: is}));
+ }
+
+ if (v.isNumber(minimum) && length < minimum) {
+ err = options.tooShort ||
+ this.tooShort ||
+ "is too short (minimum is %{count} characters)";
+ errors.push(v.format(err, {count: minimum}));
+ }
+
+ if (v.isNumber(maximum) && length > maximum) {
+ err = options.tooLong ||
+ this.tooLong ||
+ "is too long (maximum is %{count} characters)";
+ errors.push(v.format(err, {count: maximum}));
+ }
+
+ if (errors.length > 0) {
+ return options.message || errors;
+ }
+ },
+ numericality: function(value, options, attribute, attributes, globalOptions) {
+ // Empty values are fine
+ if (!v.isDefined(value)) {
+ return;
+ }
+
+ options = v.extend({}, this.options, options);
+
+ var errors = []
+ , name
+ , count
+ , checks = {
+ greaterThan: function(v, c) { return v > c; },
+ greaterThanOrEqualTo: function(v, c) { return v >= c; },
+ equalTo: function(v, c) { return v === c; },
+ lessThan: function(v, c) { return v < c; },
+ lessThanOrEqualTo: function(v, c) { return v <= c; },
+ divisibleBy: function(v, c) { return v % c === 0; }
+ }
+ , prettify = options.prettify ||
+ (globalOptions && globalOptions.prettify) ||
+ v.prettify;
+
+ // Strict will check that it is a valid looking number
+ if (v.isString(value) && options.strict) {
+ var pattern = "^-?(0|[1-9]\\d*)";
+ if (!options.onlyInteger) {
+ pattern += "(\\.\\d+)?";
+ }
+ pattern += "$";
+
+ if (!(new RegExp(pattern).test(value))) {
+ return options.message ||
+ options.notValid ||
+ this.notValid ||
+ this.message ||
+ "must be a valid number";
+ }
+ }
+
+ // Coerce the value to a number unless we're being strict.
+ if (options.noStrings !== true && v.isString(value) && !v.isEmpty(value)) {
+ value = +value;
+ }
+
+ // If it's not a number we shouldn't continue since it will compare it.
+ if (!v.isNumber(value)) {
+ return options.message ||
+ options.notValid ||
+ this.notValid ||
+ this.message ||
+ "is not a number";
+ }
+
+ // Same logic as above, sort of. Don't bother with comparisons if this
+ // doesn't pass.
+ if (options.onlyInteger && !v.isInteger(value)) {
+ return options.message ||
+ options.notInteger ||
+ this.notInteger ||
+ this.message ||
+ "must be an integer";
+ }
+
+ for (name in checks) {
+ count = options[name];
+ if (v.isNumber(count) && !checks[name](value, count)) {
+ // This picks the default message if specified
+ // For example the greaterThan check uses the message from
+ // this.notGreaterThan so we capitalize the name and prepend "not"
+ var key = "not" + v.capitalize(name);
+ var msg = options[key] ||
+ this[key] ||
+ this.message ||
+ "must be %{type} %{count}";
+
+ errors.push(v.format(msg, {
+ count: count,
+ type: prettify(name)
+ }));
+ }
+ }
+
+ if (options.odd && value % 2 !== 1) {
+ errors.push(options.notOdd ||
+ this.notOdd ||
+ this.message ||
+ "must be odd");
+ }
+ if (options.even && value % 2 !== 0) {
+ errors.push(options.notEven ||
+ this.notEven ||
+ this.message ||
+ "must be even");
+ }
+
+ if (errors.length) {
+ return options.message || errors;
+ }
+ },
+ datetime: v.extend(function(value, options) {
+ if (!v.isFunction(this.parse) || !v.isFunction(this.format)) {
+ throw new Error("Both the parse and format functions needs to be set to use the datetime/date validator");
+ }
+
+ // Empty values are fine
+ if (!v.isDefined(value)) {
+ return;
+ }
+
+ options = v.extend({}, this.options, options);
+
+ var err
+ , errors = []
+ , earliest = options.earliest ? this.parse(options.earliest, options) : NaN
+ , latest = options.latest ? this.parse(options.latest, options) : NaN;
+
+ value = this.parse(value, options);
+
+ // 86400000 is the number of milliseconds in a day, this is used to remove
+ // the time from the date
+ if (isNaN(value) || options.dateOnly && value % 86400000 !== 0) {
+ err = options.notValid ||
+ options.message ||
+ this.notValid ||
+ "must be a valid date";
+ return v.format(err, {value: arguments[0]});
+ }
+
+ if (!isNaN(earliest) && value < earliest) {
+ err = options.tooEarly ||
+ options.message ||
+ this.tooEarly ||
+ "must be no earlier than %{date}";
+ err = v.format(err, {
+ value: this.format(value, options),
+ date: this.format(earliest, options)
+ });
+ errors.push(err);
+ }
+
+ if (!isNaN(latest) && value > latest) {
+ err = options.tooLate ||
+ options.message ||
+ this.tooLate ||
+ "must be no later than %{date}";
+ err = v.format(err, {
+ date: this.format(latest, options),
+ value: this.format(value, options)
+ });
+ errors.push(err);
+ }
+
+ if (errors.length) {
+ return v.unique(errors);
+ }
+ }, {
+ parse: null,
+ format: null
+ }),
+ date: function(value, options) {
+ options = v.extend({}, options, {dateOnly: true});
+ return v.validators.datetime.call(v.validators.datetime, value, options);
+ },
+ format: function(value, options) {
+ if (v.isString(options) || (options instanceof RegExp)) {
+ options = {pattern: options};
+ }
+
+ options = v.extend({}, this.options, options);
+
+ var message = options.message || this.message || "is invalid"
+ , pattern = options.pattern
+ , match;
+
+ // Empty values are allowed
+ if (!v.isDefined(value)) {
+ return;
+ }
+ if (!v.isString(value)) {
+ return message;
+ }
+
+ if (v.isString(pattern)) {
+ pattern = new RegExp(options.pattern, options.flags);
+ }
+ match = pattern.exec(value);
+ if (!match || match[0].length != value.length) {
+ return message;
+ }
+ },
+ inclusion: function(value, options) {
+ // Empty values are fine
+ if (!v.isDefined(value)) {
+ return;
+ }
+ if (v.isArray(options)) {
+ options = {within: options};
+ }
+ options = v.extend({}, this.options, options);
+ if (v.contains(options.within, value)) {
+ return;
+ }
+ var message = options.message ||
+ this.message ||
+ "^%{value} is not included in the list";
+ return v.format(message, {value: value});
+ },
+ exclusion: function(value, options) {
+ // Empty values are fine
+ if (!v.isDefined(value)) {
+ return;
+ }
+ if (v.isArray(options)) {
+ options = {within: options};
+ }
+ options = v.extend({}, this.options, options);
+ if (!v.contains(options.within, value)) {
+ return;
+ }
+ var message = options.message || this.message || "^%{value} is restricted";
+ if (v.isString(options.within[value])) {
+ value = options.within[value];
+ }
+ return v.format(message, {value: value});
+ },
+ email: v.extend(function(value, options) {
+ options = v.extend({}, this.options, options);
+ var message = options.message || this.message || "is not a valid email";
+ // Empty values are fine
+ if (!v.isDefined(value)) {
+ return;
+ }
+ if (!v.isString(value)) {
+ return message;
+ }
+ if (!this.PATTERN.exec(value)) {
+ return message;
+ }
+ }, {
+ PATTERN: /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i
+ }),
+ equality: function(value, options, attribute, attributes, globalOptions) {
+ if (!v.isDefined(value)) {
+ return;
+ }
+
+ if (v.isString(options)) {
+ options = {attribute: options};
+ }
+ options = v.extend({}, this.options, options);
+ var message = options.message ||
+ this.message ||
+ "is not equal to %{attribute}";
+
+ if (v.isEmpty(options.attribute) || !v.isString(options.attribute)) {
+ throw new Error("The attribute must be a non empty string");
+ }
+
+ var otherValue = v.getDeepObjectValue(attributes, options.attribute)
+ , comparator = options.comparator || function(v1, v2) {
+ return v1 === v2;
+ }
+ , prettify = options.prettify ||
+ (globalOptions && globalOptions.prettify) ||
+ v.prettify;
+
+ if (!comparator(value, otherValue, options, attribute, attributes)) {
+ return v.format(message, {attribute: prettify(options.attribute)});
+ }
+ },
+ // A URL validator that is used to validate URLs with the ability to
+ // restrict schemes and some domains.
+ url: function(value, options) {
+ if (!v.isDefined(value)) {
+ return;
+ }
+
+ options = v.extend({}, this.options, options);
+
+ var message = options.message || this.message || "is not a valid url"
+ , schemes = options.schemes || this.schemes || ['http', 'https']
+ , allowLocal = options.allowLocal || this.allowLocal || false
+ , allowDataUrl = options.allowDataUrl || this.allowDataUrl || false;
+ if (!v.isString(value)) {
+ return message;
+ }
+
+ // https://gist.github.com/dperini/729294
+ var regex =
+ "^" +
+ // protocol identifier
+ "(?:(?:" + schemes.join("|") + ")://)" +
+ // user:pass authentication
+ "(?:\\S+(?::\\S*)?@)?" +
+ "(?:";
+
+ var tld = "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))";
+
+ if (allowLocal) {
+ tld += "?";
+ } else {
+ regex +=
+ // IP address exclusion
+ // private & local networks
+ "(?!(?:10|127)(?:\\.\\d{1,3}){3})" +
+ "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" +
+ "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})";
+ }
+
+ regex +=
+ // IP address dotted notation octets
+ // excludes loopback network 0.0.0.0
+ // excludes reserved space >= 224.0.0.0
+ // excludes network & broacast addresses
+ // (first & last IP address of each class)
+ "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
+ "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
+ "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
+ "|" +
+ // host name
+ "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
+ // domain name
+ "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
+ tld +
+ ")" +
+ // port number
+ "(?::\\d{2,5})?" +
+ // resource path
+ "(?:[/?#]\\S*)?" +
+ "$";
+
+ if (allowDataUrl) {
+ // RFC 2397
+ var mediaType = "\\w+\\/[-+.\\w]+(?:;[\\w=]+)*";
+ var urlchar = "[A-Za-z0-9-_.!~\\*'();\\/?:@&=+$,%]*";
+ var dataurl = "data:(?:"+mediaType+")?(?:;base64)?,"+urlchar;
+ regex = "(?:"+regex+")|(?:^"+dataurl+"$)";
+ }
+
+ var PATTERN = new RegExp(regex, 'i');
+ if (!PATTERN.exec(value)) {
+ return message;
+ }
+ },
+ type: v.extend(function(value, originalOptions, attribute, attributes, globalOptions) {
+ if (v.isString(originalOptions)) {
+ originalOptions = {type: originalOptions};
+ }
+
+ if (!v.isDefined(value)) {
+ return;
+ }
+
+ var options = v.extend({}, this.options, originalOptions);
+
+ var type = options.type;
+ if (!v.isDefined(type)) {
+ throw new Error("No type was specified");
+ }
+
+ var check;
+ if (v.isFunction(type)) {
+ check = type;
+ } else {
+ check = this.types[type];
+ }
+
+ if (!v.isFunction(check)) {
+ throw new Error("validate.validators.type.types." + type + " must be a function.");
+ }
+
+ if (!check(value, options, attribute, attributes, globalOptions)) {
+ var message = originalOptions.message ||
+ this.messages[type] ||
+ this.message ||
+ options.message ||
+ (v.isFunction(type) ? "must be of the correct type" : "must be of type %{type}");
+
+ if (v.isFunction(message)) {
+ message = message(value, originalOptions, attribute, attributes, globalOptions);
+ }
+
+ return v.format(message, {attribute: v.prettify(attribute), type: type});
+ }
+ }, {
+ types: {
+ object: function(value) {
+ return v.isObject(value) && !v.isArray(value);
+ },
+ array: v.isArray,
+ integer: v.isInteger,
+ number: v.isNumber,
+ string: v.isString,
+ date: v.isDate,
+ boolean: v.isBoolean
+ },
+ messages: {}
+ })
+ };
+
+ validate.formatters = {
+ detailed: function(errors) {return errors;},
+ flat: v.flattenErrorsToArray,
+ grouped: function(errors) {
+ var attr;
+
+ errors = v.groupErrorsByAttribute(errors);
+ for (attr in errors) {
+ errors[attr] = v.flattenErrorsToArray(errors[attr]);
+ }
+ return errors;
+ },
+ constraint: function(errors) {
+ var attr;
+ errors = v.groupErrorsByAttribute(errors);
+ for (attr in errors) {
+ errors[attr] = errors[attr].map(function(result) {
+ return result.validator;
+ }).sort();
+ }
+ return errors;
+ }
+ };
+
+ validate.exposeModule(validate, this, exports, module, __webpack_require__.amdD);
+}).call(this,
+ true ? /* istanbul ignore next */ exports : 0,
+ true ? /* istanbul ignore next */ module : 0,
+ __webpack_require__.amdD);
+
+
+/***/ })
+
+/******/ });
+/************************************************************************/
+/******/ // The module cache
+/******/ var __webpack_module_cache__ = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/ // Check if module is in cache
+/******/ var cachedModule = __webpack_module_cache__[moduleId];
+/******/ if (cachedModule !== undefined) {
+/******/ return cachedModule.exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = __webpack_module_cache__[moduleId] = {
+/******/ id: moduleId,
+/******/ loaded: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/************************************************************************/
+/******/ /* webpack/runtime/amd define */
+/******/ (() => {
+/******/ __webpack_require__.amdD = function () {
+/******/ throw new Error('define cannot be used indirect');
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/compat get default export */
+/******/ (() => {
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = (module) => {
+/******/ var getter = module && module.__esModule ?
+/******/ () => (module['default']) :
+/******/ () => (module);
+/******/ __webpack_require__.d(getter, { a: getter });
+/******/ return getter;
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/define property getters */
+/******/ (() => {
+/******/ // define getter functions for harmony exports
+/******/ __webpack_require__.d = (exports, definition) => {
+/******/ for(var key in definition) {
+/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
+/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
+/******/ }
+/******/ }
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/global */
+/******/ (() => {
+/******/ __webpack_require__.g = (function() {
+/******/ if (typeof globalThis === 'object') return globalThis;
+/******/ try {
+/******/ return this || new Function('return this')();
+/******/ } catch (e) {
+/******/ if (typeof window === 'object') return window;
+/******/ }
+/******/ })();
+/******/ })();
+/******/
+/******/ /* webpack/runtime/hasOwnProperty shorthand */
+/******/ (() => {
+/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
+/******/ })();
+/******/
+/******/ /* webpack/runtime/make namespace object */
+/******/ (() => {
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = (exports) => {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/node module decorator */
+/******/ (() => {
+/******/ __webpack_require__.nmd = (module) => {
+/******/ module.paths = [];
+/******/ if (!module.children) module.children = [];
+/******/ return module;
+/******/ };
+/******/ })();
+/******/
+/************************************************************************/
+var __webpack_exports__ = {};
+// This entry need to be wrapped in an IIFE because it need to be in strict mode.
+(() => {
+"use strict";
+/*!*******************************!*\
+ !*** ./resources/js/users.js ***!
+ \*******************************/
+__webpack_require__.r(__webpack_exports__);
+/* harmony import */ var riot__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! riot */ "./node_modules/riot/riot.esm.js");
+/* harmony import */ var _components_users_riot__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./components/users.riot */ "./resources/js/components/users.riot");
+/* harmony import */ var _components_users_form_riot__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./components/users/form.riot */ "./resources/js/components/users/form.riot");
+
+
+ // register components for buckets
+
+riot__WEBPACK_IMPORTED_MODULE_2__.register('app-users', _components_users_riot__WEBPACK_IMPORTED_MODULE_0__.default);
+riot__WEBPACK_IMPORTED_MODULE_2__.mount('app-users');
+riot__WEBPACK_IMPORTED_MODULE_2__.register('app-users-form', _components_users_form_riot__WEBPACK_IMPORTED_MODULE_1__.default);
+riot__WEBPACK_IMPORTED_MODULE_2__.mount('app-users-form');
+})();
+
+/******/ })()
+;
\ No newline at end of file
diff --git a/resources/js/components/users.riot b/resources/js/components/users.riot
new file mode 100644
index 0000000..52272ac
--- /dev/null
+++ b/resources/js/components/users.riot
@@ -0,0 +1,86 @@
+
+
+
+
+
+ { user.username }
+
+
+ { user.email }
+
+
+
+
+
+
+
state.users.length }>
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/js/components/users/form.riot b/resources/js/components/users/form.riot
new file mode 100644
index 0000000..26215ec
--- /dev/null
+++ b/resources/js/components/users/form.riot
@@ -0,0 +1,128 @@
+
+
+
+
\ No newline at end of file
diff --git a/resources/js/users.js b/resources/js/users.js
new file mode 100644
index 0000000..7e5b48e
--- /dev/null
+++ b/resources/js/users.js
@@ -0,0 +1,11 @@
+import * as riot from 'riot'
+
+import AppUsers from './components/users.riot'
+import AppUsersForm from './components/users/form.riot'
+
+// register components for buckets
+riot.register('app-users', AppUsers)
+riot.mount('app-users')
+
+riot.register('app-users-form', AppUsersForm)
+riot.mount('app-users-form')
\ No newline at end of file
diff --git a/resources/scss/components/_sidebar.scss b/resources/scss/components/_sidebar.scss
new file mode 100644
index 0000000..246895d
--- /dev/null
+++ b/resources/scss/components/_sidebar.scss
@@ -0,0 +1,68 @@
+.sidebar {
+ position: fixed;
+
+ top: 0;
+ left: 0;
+
+ width: 100%;
+ height: 100%;
+
+ max-width: 33%;
+ z-index: 1;
+
+ visibility: hidden;
+ transition: visibility 0s linear 0.5s;
+
+ &__body {
+ @extend .panel__body;
+ }
+
+ &__inner {
+ @extend .panel;
+ position: relative;
+ height: 100%;
+
+ transition: transform 0.2s;
+ transform: translateX(-100%);
+ }
+
+ &__footer {
+ position: fixed;
+ left: 0;
+ bottom: 0;
+
+ display: flex;
+ justify-content: space-between;
+
+ background: var(--background);
+ width: 100%;
+ padding: 1rem;
+ }
+
+ &:before {
+ position: fixed;
+
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: transparent;
+ transition: background-color 0.5s;
+ z-index: 0;
+
+ content: "";
+ }
+
+ &--open {
+ visibility: visible;
+ transition: visibility 0s linear 0s;
+
+ .sidebar__inner {
+ transform: translateX(0);
+ }
+
+ &:before {
+ background: rgba(0,0,0,.7);
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/scss/index.scss b/resources/scss/index.scss
index 27f7273..63296e1 100644
--- a/resources/scss/index.scss
+++ b/resources/scss/index.scss
@@ -4,6 +4,7 @@
'components/buckets',
'components/note-form',
'components/field',
+ 'components/sidebar',
'container';
.turbolinks-progress-bar {
diff --git a/resources/views/layout.html b/resources/views/layout.html
index 84a5a3c..f6e598b 100644
--- a/resources/views/layout.html
+++ b/resources/views/layout.html
@@ -23,6 +23,9 @@
Dashboard
+
+ Users
+
Settings
diff --git a/resources/views/users.html b/resources/views/users.html
new file mode 100644
index 0000000..ed9640c
--- /dev/null
+++ b/resources/views/users.html
@@ -0,0 +1,15 @@
+<% layout('./layout.html') %>
+
+
+
+
\ No newline at end of file
diff --git a/server-backup.ts b/server-backup.ts
new file mode 100644
index 0000000..6c9e138
--- /dev/null
+++ b/server-backup.ts
@@ -0,0 +1,57 @@
+import 'https://deno.land/x/dotenv@v2.0.0/load.ts'
+import {
+ opine,
+ serveStatic,
+ json,
+ urlencoded
+} from 'https://deno.land/x/opine@1.5.3/mod.ts'
+import { dirname, join, createError } from "https://deno.land/x/opine@1.5.3/deps.ts";
+import { renderFile } from 'https://deno.land/x/eta@v1.12.2/mod.ts'
+
+// middleware
+import session from './src/middleware/session.ts'
+
+// getting routes
+import index from './src/http/index.ts'
+//import bucket from './src/http/bucket.ts'
+//import settings from './src/http/settings.ts'
+import users from './src/http/users.ts'
+
+//import bucketApi from './src/http/api/bucket.ts'
+//import noteApi from './src/http/api/note.ts'
+
+const app = opine()
+const __dirname = dirname(import.meta.url)
+
+// for parsing application/json
+app.use(json())
+
+// for parsing application/x-www-form-urlencoded
+app.use(urlencoded())
+
+// adding static files
+app.use(serveStatic(join(__dirname, 'public')))
+
+// adding eta as view engine
+app.engine('.html', renderFile)
+app.set('views', join(__dirname, 'resources/views'))
+app.set('view engine', 'html')
+
+// adding http classes for routes
+app.use('*', session)
+
+app.use('/', index)
+//app.use('/bucket', bucket)
+//app.use('/settings', settings)
+app.use('/users', users)
+//app.use('/api/bucket', bucketApi)
+//app.use('/api/note', noteApi)
+
+app.use((request, response, next) => {
+ response.setStatus(404)
+ response.render('errors/404')
+})
+
+// let it rain
+app.listen(Number(Deno.env.get('SERVER_PORT')))
+console.log('running on ' + Deno.env.get('SERVER_PORT'))
\ No newline at end of file
diff --git a/server.ts b/server.ts
index 44921ef..be24fe2 100644
--- a/server.ts
+++ b/server.ts
@@ -8,22 +8,28 @@ import {
import { dirname, join, createError } from "https://deno.land/x/opine@1.5.3/deps.ts";
import { renderFile } from 'https://deno.land/x/eta@v1.12.2/mod.ts'
+// middleware
+import session from './src/middleware/session.ts'
+
// getting routes
-import index from './src/http/index.ts'
-import bucket from './src/http/bucket.ts'
-import settings from './src/http/settings.ts'
+import home from './src/http/home.ts'
+import buckets from './src/http/buckets.ts'
+
+//import settings from './src/http/settings.ts'
+import users from './src/http/users.ts'
+import usersApi from './src/http/api/users.ts'
-import bucketApi from './src/http/api/bucket.ts'
-import noteApi from './src/http/api/note.ts'
+//import bucketApi from './src/http/api/bucket.ts'
+//import noteApi from './src/http/api/note.ts'
const app = opine()
const __dirname = dirname(import.meta.url)
// for parsing application/json
-app.use(json());
+app.use(json())
// for parsing application/x-www-form-urlencoded
-app.use(urlencoded());
+app.use(urlencoded())
// adding static files
app.use(serveStatic(join(__dirname, 'public')))
@@ -34,11 +40,18 @@ app.set('views', join(__dirname, 'resources/views'))
app.set('view engine', 'html')
// adding http classes for routes
-app.use('/', index)
-app.use('/bucket', bucket)
-app.use('/settings', settings)
-app.use('/api/bucket', bucketApi)
-app.use('/api/note', noteApi)
+app.use('*', session)
+
+app.use('/', home)
+
+app.use('/buckets', buckets)
+
+app.use('/users', users)
+app.use('/api/users', usersApi)
+
+//app.use('/api/bucket', bucketApi)
+//app.use('/api/note', noteApi)
+
app.use((request, response, next) => {
response.setStatus(404)
response.render('errors/404')
diff --git a/src/http/api/bucket.ts b/src/http/api/bucket.ts
index b24ffb7..432a4a9 100644
--- a/src/http/api/bucket.ts
+++ b/src/http/api/bucket.ts
@@ -38,7 +38,10 @@ router.get('/', async function(request, response)
*/
router.get('/:uuid', function(request, response)
{
-
+ response.render('bucket/form', {
+ visiblities: visibilties.enums,
+ types: types.enums
+ })
})
/**
@@ -50,7 +53,47 @@ router.get('/:uuid', function(request, response)
*/
router.post('/', function(request, response, next)
{
+ const body = request.body
+
+ let typeValues = []
+ let visiblityValues = []
+
+ types.enums.forEach(function(type) {
+ typeValues.push(String(type.value))
+ })
+
+ visibilties.enums.forEach(function(visiblity) {
+ visiblityValues.push(String(visiblity.value))
+ })
+ // escape before validate
+ if (body.title) {
+ body.title = escapeHtml(body.title)
+ }
+
+ // escape before validate
+ if (body.description) {
+ body.description = escapeHtml(body.description)
+ }
+
+ // validate
+ const [ valid, errors ] = await validate(body, {
+ title: [ required, maxLength(255) ],
+ description: [ required, maxLength(255) ],
+ type: [ required, isIn(typeValues) ],
+ visiblity: [ required, isIn(visiblityValues)]
+ });
+
+ if (valid) {
+ const db = new Database('./storage/database/buckets.json')
+
+ body._id = v4.generate()
+ const bucket = await db.insertOne(body)
+
+ response.redirect('/buckets/' + bucket._id)
+ } else {
+ response.redirect('/buckets/create')
+ }
})
/**
@@ -85,7 +128,7 @@ router.delete('/:uuid', async function(request, response, next)
const db = new Database('./storage/database/buckets.json')
const bucket = await db.deleteOne({ _id: request.params.uuid });
- // check if bucket is deleted
+ // check if bucket is deleted
if (bucket) {
result = true
}
diff --git a/src/http/bucket.ts b/src/http/api/buckets.ts
similarity index 58%
rename from src/http/bucket.ts
rename to src/http/api/buckets.ts
index 57a3666..8de49fb 100644
--- a/src/http/bucket.ts
+++ b/src/http/api/buckets.ts
@@ -1,22 +1,42 @@
import { Router } from 'https://deno.land/x/opine@1.5.3/mod.ts'
+
import { v4 } from "https://deno.land/std@0.99.0/uuid/mod.ts";
import { validate, required, isIn, maxLength } from 'https://deno.land/x/validasaur@v0.15.0/mod.ts'
+
import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'
-import { escapeHtml } from "https://deno.land/x/escape@1.3.0/mod.ts"
+import { BucketSchema } from './../../stores/bucket.ts'
-import { BucketSchema } from './../stores/bucket.ts'
-import { visibilties, types } from './../enums/bucket.ts'
+import { visibilties, types } from './../../enums/bucket.ts'
const router = Router()
/**
- * render template for form
+ *
*
* @param request
* @param response
* @return
*/
-router.get('/create', function(request, response, next)
+router.get('/', async function(request, response)
+{
+ const db = new Database('./storage/database/buckets.json')
+ const buckets = await db.findMany({
+ 'type': request.params.visiblity
+ })
+
+ response.json({
+ data: buckets
+ })
+})
+
+/**
+ *
+ *
+ * @param request
+ * @param response
+ * @return
+ */
+router.get('/:uuid', function(request, response)
{
response.render('bucket/form', {
visiblities: visibilties.enums,
@@ -25,13 +45,13 @@ router.get('/create', function(request, response, next)
})
/**
- * render template for form
+ *
*
* @param request
* @param response
* @return
*/
-router.post('/store', async function(request, response, next)
+router.post('/', function(request, response, next)
{
const body = request.body
@@ -56,7 +76,7 @@ router.post('/store', async function(request, response, next)
body.description = escapeHtml(body.description)
}
- // validate
+ // validate
const [ valid, errors ] = await validate(body, {
title: [ required, maxLength(255) ],
description: [ required, maxLength(255) ],
@@ -70,34 +90,51 @@ router.post('/store', async function(request, response, next)
body._id = v4.generate()
const bucket = await db.insertOne(body)
- response.redirect('/bucket/' + bucket._id)
+ response.redirect('/buckets/' + bucket._id)
} else {
- response.redirect('/bucket/create')
+ response.redirect('/buckets/create')
}
})
/**
- * render template for form
+ *
*
* @param request
* @param response
* @return
*/
-router.get('/:id', async function(request, response, next)
+router.put('/:uuid', function(request, response, next)
{
- if (!v4.validate(request.params.id)) {
+
+})
+
+/**
+ * delete single bucket
+ *
+ * @param request
+ * @param response
+ * @return
+ */
+router.delete('/:uuid', async function(request, response, next)
+{
+ // check if uuid is valid
+ if (!v4.validate(request.params.uuid)) {
response.setStatus(404)
}
+ let result = false
+
+ // getting database and search by uuid
const db = new Database('./storage/database/buckets.json')
- const bucket = await db.findOne({ '_id': request.params.id })
+ const bucket = await db.deleteOne({ _id: request.params.uuid });
- if (!bucket) {
- response.setStatus(404)
+ // check if bucket is deleted
+ if (bucket) {
+ result = true
}
- response.render('bucket/single', {
- bucket: bucket
+ response.json({
+ 'success': result
})
})
diff --git a/src/http/api/note.ts b/src/http/api/note.ts
index ae87fe9..c0e7ad0 100644
--- a/src/http/api/note.ts
+++ b/src/http/api/note.ts
@@ -19,31 +19,14 @@ const router = Router()
router.post('/:bucket_id', async function(request, response)
{
const body = request.body
- let note
-
- // check if uuid is valid
- if (!v4.validate(request.params.bucket_id)) {
- response.setStatus(404)
- }
-
- // check if bucket exists
- const db = new Database('./storage/database/buckets.json')
- const bucket = await db.findOne({ _id: request.params.bucket_id })
-
- if (!bucket) {
- response.setStatus(404)
- }
const [ valid, errors ] = await validate(body, {
title: [ maxLength(255) ],
content: [ maxLength(10922) ]
})
- if (valid && bucket) {
- body._id = v4.generate()
-
- // getting database and search by uuid
- const db = new Database('./storage/database/' + bucket._id + '.json')
+ if (valid) {
+ const noteRepository = new NoteRepository(request.bucket._id)
note = await db.insertOne(body)
}
diff --git a/src/http/api/notes.ts b/src/http/api/notes.ts
new file mode 100644
index 0000000..c0e7ad0
--- /dev/null
+++ b/src/http/api/notes.ts
@@ -0,0 +1,93 @@
+import { v4 } from "https://deno.land/std@0.99.0/uuid/mod.ts";
+import { validate, required, isIn, maxLength } from 'https://deno.land/x/validasaur@v0.15.0/mod.ts'
+
+import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'
+import { BucketSchema } from './../../stores/bucket.ts'
+import { NoteSchema } from './../../stores/note.ts'
+
+import { Router } from 'https://deno.land/x/opine@1.5.3/mod.ts'
+
+const router = Router()
+
+/**
+ *
+ *
+ * @param request
+ * @param response
+ * @return
+ */
+router.post('/:bucket_id', async function(request, response)
+{
+ const body = request.body
+
+ const [ valid, errors ] = await validate(body, {
+ title: [ maxLength(255) ],
+ content: [ maxLength(10922) ]
+ })
+
+ if (valid) {
+ const noteRepository = new NoteRepository(request.bucket._id)
+ note = await db.insertOne(body)
+ }
+
+ response.json({
+ data: note
+ })
+})
+
+/**
+ *
+ *
+ * @param request
+ * @param response
+ * @return
+ */
+router.put('/:bucket_id', async function(request, response, next)
+{
+ const body = request.body
+
+ let note
+
+ // check if uuid is valid
+ if (!v4.validate(request.params.bucket_id)) {
+ response.setStatus(404)
+ }
+
+ // check if bucket exists
+ const bucketDb = new Database('./storage/database/buckets.json')
+ const bucket = await bucketDb.findOne({ _id: request.params.bucket_id })
+
+ if (!bucket) {
+ response.setStatus(404)
+ }
+
+ const [ valid, errors ] = await validate(body, {
+ title: [ maxLength(255) ],
+ content: [ maxLength(10922) ]
+ })
+
+ if (valid && bucket) {
+
+ // getting database and search by uuid
+ const db = new Database('./storage/database/' + bucket._id + '.json')
+ note = await db.updateOne({ _id: body._id }, body)
+ }
+
+ response.json({
+ data: note
+ })
+})
+
+/**
+ *
+ *
+ * @param request
+ * @param response
+ * @return
+ */
+router.delete('/:bucket_id/:id', function(request, response, next)
+{
+
+})
+
+export default router
\ No newline at end of file
diff --git a/src/http/api/user.ts b/src/http/api/user.ts
new file mode 100644
index 0000000..3e8975e
--- /dev/null
+++ b/src/http/api/user.ts
@@ -0,0 +1,149 @@
+import { v4 } from "https://deno.land/std@0.99.0/uuid/mod.ts";
+import { validate, required, isIn, maxLength } from 'https://deno.land/x/validasaur@v0.15.0/mod.ts'
+
+import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'
+import { UserSchema } from './../../stores/user.ts'
+
+import { Router } from 'https://deno.land/x/opine@1.5.3/mod.ts'
+
+const router = Router()
+
+
+/**
+ * get all users
+ *
+ * @param request
+ * @param response
+ * @return
+ *
+ */
+router.get('/', async function(request, response)
+{
+ const userRepository = new UserRepository()
+ const users = await userRepository.db.findAll()
+
+ response.json({
+ data: user
+ })
+})
+
+/**
+ * create user
+ *
+ * @param request
+ * @param response
+ * @return
+ *
+ */
+router.get('/:id', async function(request, response)
+{
+ const userRepository = new UserRepository()
+
+ const [ valid, errors ] = await validate(request.params, {
+ id: [ required, uuid ]
+ })
+
+ if (valid) {
+ const user = await userRepository.db.findOne({
+ '_id': request.params.id
+ })
+
+ if (!user) {
+ response.setStatus(404)
+ }
+
+ } else {
+ response.setStatus(405)
+ }
+
+ response.json({
+ data: user
+ })
+})
+
+/**
+ * create user
+ *
+ * @param request
+ * @param response
+ * @return
+ *
+ */
+router.post('/', async function(request, response)
+{
+ const body = request.body
+ const userRepository = new UserRepository()
+
+ const [ valid, errors ] = await validate(body, {
+ email: [ email, required ]
+ })
+
+ if (valid) {
+ user = await userRepository.create(body)
+ }
+
+ response.json({
+ data: user
+ })
+})
+
+/**
+ *
+ *
+ * @param request
+ * @param response
+ * @return
+ */
+router.put('/:id', async function(request, response, next)
+{
+ const body = request.body
+ const userRepository = new UserRepository()
+
+ let user = false
+
+ const [ valid, errors ] = await validate(body, {
+ _id: [ required, uuid, exists ],
+ email: [ email, required, unique ],
+ password: [ maxLength(64) ],
+ displayname: [ maxLength(128) ],
+ role: [ array() ]
+ })
+
+ if (valid) {
+ user = userRepository.update(body)
+ }
+
+ response.json({
+ data: user
+ })
+})
+
+/**
+ *
+ *
+ * @param request
+ * @param response
+ * @return
+ *
+ */
+router.delete('/:id', function(request, response)
+{
+ const userRepository = new UserRepository()
+ let user = false
+
+ const [ valid, errors ] = await validate(request.params, {
+ '_id': [ required, uuid, exists ]
+ })
+
+ if (valid) {
+ user = userRepository.db.deleteOne({
+ '_id': request.params._id
+ })
+ }
+
+ response.json({
+ data: user
+ })
+})
+
+export default router
\ No newline at end of file
diff --git a/src/http/api/users.ts b/src/http/api/users.ts
new file mode 100644
index 0000000..54b76fa
--- /dev/null
+++ b/src/http/api/users.ts
@@ -0,0 +1,60 @@
+import { Router } from 'https://deno.land/x/opine@1.5.3/mod.ts'
+import UserRepository from '../../repositories/user.ts'
+
+import { validate, required, maxLength, isEmail } from 'https://deno.land/x/validasaur@v0.15.0/mod.ts'
+
+const router = Router()
+
+
+/**
+ * get all users
+ *
+ * @param request
+ * @param response
+ * @return
+ *
+ */
+router.get('/', async function(request, response)
+{
+ const userRepository = new UserRepository()
+ const users = await userRepository.db.findMany()
+
+ response.json({
+ data: users
+ })
+})
+
+/**
+ * create user
+ *
+ * @param request
+ * @param response
+ * @return
+ *
+ */
+router.post('/', async function(request, response)
+{
+ const body = request.body
+ const userRepository = new UserRepository()
+
+ let user = []
+
+ const [ valid, errors ] = await validate(body, {
+ email: [ isEmail, maxLength(255), required ],
+ password: [ required, maxLength(64) ]
+ })
+
+ if (valid) {
+ user = await userRepository.create(body)
+
+ // remove password
+ // @TODO make sure repository can hide variables
+ delete user.password
+ }
+
+ response.json({
+ data: user
+ })
+})
+
+export default router
\ No newline at end of file
diff --git a/src/http/auth.ts b/src/http/auth.ts
new file mode 100644
index 0000000..5d575e0
--- /dev/null
+++ b/src/http/auth.ts
@@ -0,0 +1,43 @@
+import { validate, required, email } from 'https://deno.land/x/validasaur@v0.15.0/mod.ts'
+import { Router } from 'https://deno.land/x/opine@1.5.3/mod.ts'
+
+const router = Router()
+
+/**
+ * auth user, check for password in db
+ * and create jwt
+ *
+ * @param request
+ * @param response
+ * @return
+ */
+router.post('/', async function(request, response)
+{
+ if (body.password) {
+ body.password = escapeHtml(body.password)
+ }
+
+ const [ valid, errors ] = await validate(body, {
+ email: [ required, email ],
+ password: [ required, maxLength(64) ]
+ });
+
+ if (valid) {
+
+ // check if user exists
+ user = await userRepository.db.findOne({
+ 'email': body.email
+ })
+
+ if (user) {
+ result = userRepository.verifyPassword(user, body.password)
+
+ if (result) {
+
+ response.cookie('auth-token', jwt)
+ }
+ }
+ }
+
+ response.redirect('/')
+}
diff --git a/src/http/buckets.ts b/src/http/buckets.ts
new file mode 100644
index 0000000..02e23e0
--- /dev/null
+++ b/src/http/buckets.ts
@@ -0,0 +1,23 @@
+import { Router } from 'https://deno.land/x/opine@1.5.3/mod.ts'
+import bucketMiddleware from '../middleware/bucket.ts'
+
+const router = Router()
+
+// check for id and try load bucket
+router.param('bucket_id', bucketMiddleware)
+
+/**
+ * render single bucket
+ *
+ * @param request
+ * @param response
+ * @return
+ */
+router.get('/:bucket_id', async function(request, response)
+{
+ response.render('bucket/single', {
+ bucket: response.locals.bucket
+ })
+})
+
+export default router
\ No newline at end of file
diff --git a/src/http/index.ts b/src/http/home.ts
similarity index 100%
rename from src/http/index.ts
rename to src/http/home.ts
diff --git a/src/http/users.ts b/src/http/users.ts
new file mode 100644
index 0000000..85e790c
--- /dev/null
+++ b/src/http/users.ts
@@ -0,0 +1,17 @@
+import { Router } from 'https://deno.land/x/opine@1.5.3/mod.ts'
+
+const router = Router()
+
+/**
+ * render template for settings
+ *
+ * @param request
+ * @param response
+ * @return
+ */
+router.get('/', function(request, response, next)
+{
+ response.render('users')
+})
+
+export default router
\ No newline at end of file
diff --git a/src/middleware/bucket.ts b/src/middleware/bucket.ts
new file mode 100644
index 0000000..9e8f6df
--- /dev/null
+++ b/src/middleware/bucket.ts
@@ -0,0 +1,51 @@
+import { Router } from 'https://deno.land/x/opine@1.5.3/mod.ts'
+import { validate, required } from 'https://deno.land/x/validasaur@v0.15.0/mod.ts'
+import { uuid } from '../rules/uuid.ts'
+
+import BucketRepository from '../repositories/bucket.ts'
+
+const router = Router()
+
+/**
+ * check every route for single bucket
+ *
+ * @param request
+ * @param response
+ * @param next
+ * @return
+ */
+async function bucketMiddleware(request: any, response: any, next: any)
+{
+ // max for id
+ request.params.bucket_id = request.params.bucket_id.slice(0, 128)
+
+ // only dash, numbers & letters are allowed
+ request.params.bucket_id = request.params.bucket_id.replace(/[^a-z0-9-]/gi, '')
+
+ const [ valid, errors ] = await validate(request.params, {
+ bucket_id: [ uuid ]
+ })
+
+ // if invalid send 404
+ if (!valid) {
+ response
+ .setStatus(404)
+ .send()
+ }
+
+ // getting
+ const bucketRepository = new BucketRepository()
+ const bucket = await bucketRepository.db.findOne({ '_id': request.params.bucket_id })
+
+ // if not exists send 404
+ if (!bucket) {
+ response
+ .setStatus(404)
+ .send()
+ }
+
+ response.locals.bucket = bucket
+ next()
+}
+
+export default bucketMiddleware
\ No newline at end of file
diff --git a/src/middleware/session.ts b/src/middleware/session.ts
new file mode 100644
index 0000000..a6fbdf2
--- /dev/null
+++ b/src/middleware/session.ts
@@ -0,0 +1,32 @@
+import { Router } from 'https://deno.land/x/opine@1.5.3/mod.ts'
+import { v4 } from 'https://deno.land/std@0.99.0/uuid/mod.ts'
+
+import UserRepository from './../repositories/user.ts'
+
+export default async function(request: any, response: any, next: any)
+{
+ request.user = false
+
+ try {
+ const token = request.headers.authorization.split(' ')[1];
+
+ // if v4 is not validate next
+ if (!v4.validate(token)) {
+ next()
+ }
+
+ // search for user with session id
+ const userRepository = new UserRepository()
+ const user = await userRepository.db.findOne({
+ 'session_id': token
+ })
+
+ if (user) {
+ response.locals.user = user
+ }
+ } catch(error) {
+
+ }
+
+ next()
+}
\ No newline at end of file
diff --git a/src/repositories/bucket.ts b/src/repositories/bucket.ts
new file mode 100644
index 0000000..29536ed
--- /dev/null
+++ b/src/repositories/bucket.ts
@@ -0,0 +1,36 @@
+import { v4 } from 'https://deno.land/std@0.99.0/uuid/mod.ts'
+import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'
+import { BucketSchema } from '../stores/bucket.ts'
+
+/**
+ *
+ *
+ */
+class BucketRepository
+{
+ db: any
+
+ constructor()
+ {
+ this.db = new Database('./storage/database/buckets.json')
+ }
+
+ /**
+ *
+ */
+ async create(data: any)
+ {
+ data._id = v4.generate()
+ return await this.db.insertOne(data)
+ }
+
+ /**
+ *
+ */
+ async update(data: any)
+ {
+ return await this.db.updateOne({ '_id': data._id }, data)
+ }
+}
+
+export default BucketRepository
diff --git a/src/repositories/user.ts b/src/repositories/user.ts
new file mode 100644
index 0000000..efc56ad
--- /dev/null
+++ b/src/repositories/user.ts
@@ -0,0 +1,47 @@
+import * as bcrypt from 'https://deno.land/x/bcrypt@v0.2.4/mod.ts'
+import { v4 } from 'https://deno.land/std@0.99.0/uuid/mod.ts'
+
+import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'
+import { UserSchema } from './../stores/user.ts'
+
+/**
+ *
+ *
+ */
+class UserRepository
+{
+ db: any;
+
+ constructor()
+ {
+ this.db = new Database('./../../storage/database/users.json')
+ }
+
+ /**
+ *
+ */
+ async create(data: any)
+ {
+ data._id = v4.generate()
+ data.password = await bcrypt.hash(data.password)
+
+ return await this.db.insertOne(data)
+ }
+
+ /**
+ *
+ */
+ async update(data: any)
+ {
+ const user = await this.db.findOne({ '_id': data._id })
+
+ // if password has changed hash password new
+ if (user && user.password !== data.password) {
+ data.password = await bcrypt.hash(data.password)
+ }
+
+ return await this.db.updateOne({ '_id': data._id }, data)
+ }
+}
+
+export default UserRepository
diff --git a/src/respositories/user.ts b/src/respositories/user.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/src/rules/exists.ts b/src/rules/exists.ts
new file mode 100644
index 0000000..4b2bd35
--- /dev/null
+++ b/src/rules/exists.ts
@@ -0,0 +1,23 @@
+import { invalid, Validity, Rule } from "https://deno.land/x/validasaur/mod.ts";
+
+/**
+ *
+ * @param id
+ * @return
+ */
+export function exists(id: string): Rule {
+ return async function existsRule(value: any): Promise {
+
+ if (typeof value !== 'string' && typeof value !== 'number') {
+ return invalid('exists', { value, table, column });
+ }
+
+ const data = await db.findOne({
+ key: value
+ })
+
+ if (data !== null) {
+ return invalid('unique', { value, table, column });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/rules/unique.ts b/src/rules/unique.ts
new file mode 100644
index 0000000..05ce4ca
--- /dev/null
+++ b/src/rules/unique.ts
@@ -0,0 +1,25 @@
+import { invalid, Validity, Rule } from "https://deno.land/x/validasaur/mod.ts";
+
+/**
+ * search for key
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+export function unique(key: string, value: string): Rule {
+ return async function uniqueRule(value: any): Promise {
+
+ if (typeof value !== 'string' && typeof value !== 'number') {
+ return invalid('unique', { value, table, column });
+ }
+
+ const data = await db.findOne({
+ key: value
+ })
+
+ if (data !== null) {
+ return invalid('unique', { value, table, column });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/rules/uuid.ts b/src/rules/uuid.ts
new file mode 100644
index 0000000..34907e7
--- /dev/null
+++ b/src/rules/uuid.ts
@@ -0,0 +1,19 @@
+import { invalid, Validity } from 'https://deno.land/x/validasaur@v0.15.0/mod.ts'
+import { v4 } from 'https://deno.land/std@0.99.0/uuid/mod.ts'
+
+/**
+ * validate uuid v4
+ *
+ * @param value
+ * @return
+ *
+ */
+export async function uuid(value: any): Promise {
+ if (typeof value !== 'string') {
+ return invalid('uuid', { value: value })
+ }
+
+ if (!v4.validate(value)) {
+ return invalid('uuid', { value: value })
+ }
+}
\ No newline at end of file
diff --git a/src/stores/user.ts b/src/stores/user.ts
index 10ee638..fe32ad3 100644
--- a/src/stores/user.ts
+++ b/src/stores/user.ts
@@ -3,5 +3,7 @@ interface UserSchema {
email: string;
password: string;
displayname: string;
- is_admin: boolean
+ session_id: string;
+ session_expired: string;
+ role: string[]
}
diff --git a/src/validators/bucket.ts b/src/validators/bucket.ts
deleted file mode 100644
index 59c06c0..0000000
--- a/src/validators/bucket.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { validate, required, isNumber } from 'https://deno.land/x/validasaur@0.15.0/mod.ts'
-
-// create shema for validation
-const BucketSchema = Schema({
- title: string.trim().normalize(),
- description: string.trim().normalize().optional(),
- type: Schema.either('a', 'b', 'c'),
- visiblity: Schema.either('a', 'b', 'c')
-})
-
-// create type and get validator from schema
-export type bucketType = Type
-export const bucketValidator = BucketSchema.destruct()
\ No newline at end of file
diff --git a/src/validators/note.ts b/src/validators/note.ts
deleted file mode 100644
index 4b57d0d..0000000
--- a/src/validators/note.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import Schema, { Type, string, number, array } from 'https://denoporter.sirjosh.workers.dev/v1/deno.land/x/computed_types/src/index.ts'
-
-// create shema for validation
-const NoteSchema = Schema({
- title: string.trim().normalize(),
- description: string.trim().normalize().optional(),
- type: Schema.either('a', 'b', 'c'),
- visiblity: Schema.either('a', 'b', 'c')
-})
-
-// create type and get validator from schema
-type Note = Type
-const validator = NoteSchema.destruct()
-
-export default validator
\ No newline at end of file
diff --git a/src/validators/user.ts b/src/validators/user.ts
deleted file mode 100644
index 2837356..0000000
--- a/src/validators/user.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import Schema, { Type, string, number, array } from 'https://denoporter.sirjosh.workers.dev/v1/deno.land/x/computed_types/src/index.ts'
-
-// create shema for validation
-const UserSchema = Schema({
- username: string.trim().normalize(),
- password: string.normalize(),
- email: Schema.email()
-})
-
-// create type and get validator from schema
-type User = Type
-const validator = UserSchema.destruct()
-
-export default validator
\ No newline at end of file
diff --git a/webpack.mix.js b/webpack.mix.js
index c59ed56..b6b11e0 100644
--- a/webpack.mix.js
+++ b/webpack.mix.js
@@ -62,6 +62,7 @@ mix
.js('resources/js/bucket-single.js', 'public/js')
.js('resources/js/create-bucket.js', 'public/js')
.js('resources/js/dashboard.js', 'public/js')
+ .js('resources/js/users.js', 'public/js')
.sass('resources/scss/index.scss', 'public/css')
.copy(
'node_modules/@tentakelfabrik/plain-ui/src/fonts/*',