<commit_before>import Icon from '@conveyal/woonerf/components/icon'
import React, {Component, PropTypes} from 'react'
import Button from 'react-bootstrap/lib/Button'
import {shallowEqual} from 'react-pure-render'
import {Table, Column} from 'react-virtualized/dist/commonjs/Table'

import EntityListButtons from './EntityListButtons'
import EntityListSecondaryActions from './EntityListSecondaryActions'
import {getEntityName} from '../util/gtfs'

export default class EntityList extends Component {
  static propTypes = {
    activeComponent: PropTypes.string.isRequired,
    activeEntity: PropTypes.object,
    activeEntityId: PropTypes.string,
    deleteEntity: PropTypes.func.isRequired,
    enterTimetableEditor: PropTypes.func,
    entities: PropTypes.array,
    feedSource: PropTypes.object,
    newGtfsEntity: PropTypes.func.isRequired,
    setActiveEntity: PropTypes.func.isRequired,
    updateActiveEntity: PropTypes.func.isRequired,
    width: PropTypes.number.isRequired
  }

  state = {}

  _onResize = () => {
    this.setState({width: window.innerWidth, height: window.innerHeight})
  }

  componentWillMount () {
    this._onResize()
  }

  componentDidMount () {
    window.addEventListener('resize', this._onResize)
  }

  componentWillUnmount () {
    window.removeEventListener('resize', this._onResize)
  }

  componentWillReceiveProps (nextProps) {
    let fromIndex, toIndex
    if (nextProps.activeComponent !== this.props.activeComponent) {
      this.setState({fromIndex, toIndex})
    }
  }

  shouldComponentUpdate (nextProps) {
    // simply running shallowEqual on all props does not give us the performance we need here
    // (especially with many, many stops)
    return !shallowEqual(nextProps.entities, this.props.entities) ||
    !shallowEqual(nextProps.activeEntity, this.props.activeEntity) ||
    !shallowEqual(nextProps.activeEntityId, this.props.activeEntityId) ||
    !shallowEqual(nextProps.activeComponent, this.props.activeComponent) ||
    !shallowEqual(nextProps.feedSource, this.props.feedSource)
  }

  _optionRenderer = ({ focusedOption, focusedOptionIndex, focusOption, key, labelKey, option, options, selectValue, style, valueArray }) => {
    const className = ['VirtualizedSelectOption']
    if (option === focusedOption) {
      className.push('VirtualizedSelectFocusedOption')
    }
    if (option.disabled) {
      className.push('VirtualizedSelectDisabledOption')
    }
    const events = option.disabled
      ? {}
      : {
        onClick: () => selectValue(option),
        onMouseOver: () => focusOption(option)
      }
    return (

          {option.label}

    )
  }

  _getRowStyle (index, list) {
    const activeColor = '#F2F2F2'
    const rowStyle = {
      borderBottom: 'solid 1px #ddd',
      cursor: 'pointer',
      outline: 'none'
    }
    if (list[index] && (list[index].isActive || list[index].isSelected)) {
      rowStyle.backgroundColor = activeColor
    }
    return rowStyle
  }

  updateIndexes = (fromIndex, toIndex) => {
    this.setState({fromIndex, toIndex})
  }

  _onClickTimetableEditor = () => this.props.enterTimetableEditor()

  _onClickNew = () => this.props.newGtfsEntity(this.props.feedSource.id, this.props.activeComponent)

  _onRowClick (index, list, shiftKey) {
    let fromIndex, toIndex
    if (shiftKey && this.props.activeEntity && !list[index].isActive) {
      const selectedIndex = list.findIndex(e => e.id === this.props.activeEntity.id)
      fromIndex = selectedIndex > index ? index : selectedIndex
      toIndex = selectedIndex < index ? index : selectedIndex
      console.log(`select multiple from ${fromIndex} to ${toIndex}`)
      this.props.setActiveEntity(this.props.feedSource.id, this.props.activeComponent)
    } else
    if (list[index].isActive) {
      this.props.setActiveEntity(this.props.feedSource.id, this.props.activeComponent)
    } else {
      this.props.setActiveEntity(this.props.feedSource.id, this.props.activeComponent, list[index])
    }
    this.setState({fromIndex, toIndex})
  }
  // TODO: add hover to row rendering
  // _rowRenderer (props) {
  //
  // }
  render () {
    const {
      tableView,
      width,
      activeEntity,
      activeComponent,
      hasRoutes,
      feedSource,
      entities,
      enterTimetableEditor
    } = this.props
    const sidePadding = '5px'
    const panelWidth = !tableView ? `${width}px` : '100%'
    const panelStyle = {
      width: panelWidth,
      height: '100%',
      position: 'absolute',
      left: '0px',
      zIndex: 1,
      backgroundColor: 'white',
      paddingRight: '0px',
      paddingLeft: sidePadding
    }
    let activeIndex
    const list = entities && entities.length
      ? entities.map((entity, index) => {
        if (activeEntity && entity.id === activeEntity.id) {
          activeIndex = index
        }
        const isActive = activeEntity && entity.id === activeEntity.id
        const isSelected = typeof this.state.fromIndex !== 'undefined' && typeof this.state.toIndex !== 'undefined' && index >= this.state.fromIndex && index <= this.state.toIndex
        const name = getEntityName(activeComponent, entity) || '[Unnamed]'
        return {name, id: entity.id, isActive, isSelected}
      }
    )
    : []
    const entityList = list.length
    ? <Table
      width={width - 5}
      height={560}
      key={`${feedSource.id}-${activeComponent}-table`}
      disableHeader
      headerHeight={20}
      rowHeight={25}
      scrollToIndex={activeIndex}
      rowClassName='noselect'
      rowStyle={({index}) => this._getRowStyle(index, list)}
      rowCount={list.length}
      onRowClick={({event, index}) => {
        this._onRowClick(index, list, event.shiftKey)
      }}
      rowGetter={({index}) => list[index]}
      >
      <Column
        label='Name'
        dataKey='name'
        className='small entity-list-row'
        style={{outline: 'none'}}
        width={width - 5}
      />

      :

        <Button
          bsSize='small'
          disabled={entities && entities.findIndex(e => e.id === 'new') !== -1}
          onClick={this._onClickNew}>
          <Icon type='plus' /> Create first {activeComponent === 'scheduleexception' ? 'exception' : activeComponent}
        </Button>

    return (

          <EntityListButtons
            fromIndex={this.state.fromIndex}
            toIndex={this.state.toIndex}
            list={list}
            updateIndexes={this.updateIndexes}
            {...this.props}
          />
          {activeComponent === 'calendar' || activeComponent === 'scheduleexception'
            ? <Button
              style={{marginTop: '10px'}}
              block
              disabled={!hasRoutes}
              onClick={this._onClickTimetableEditor}
            >
              <Icon type='pencil' /> Edit schedules
            </Button>
            : null
          }
          {/* Table view button */}

        <EntityListSecondaryActions {...this.props} />
        {!tableView
          ? entityList
          : null // GtfsTable fully removed from repo, last available at fe29528569f5f64c23a49d2af0bd224f3d63d010
        }

    )
  }
}
<commit_msg>style(EntityList): fix lint<commit_after>import Icon from '@conveyal/woonerf/components/icon'
import React, {Component, PropTypes} from 'react'
import Button from 'react-bootstrap/lib/Button'
import {shallowEqual} from 'react-pure-render'
import {Table, Column} from 'react-virtualized/dist/commonjs/Table'

import EntityListButtons from './EntityListButtons'
import EntityListSecondaryActions from './EntityListSecondaryActions'
import {getEntityName} from '../util/gtfs'

export default class EntityList extends Component {
  static propTypes = {
    activeComponent: PropTypes.string.isRequired,
    activeEntity: PropTypes.object,
    activeEntityId: PropTypes.string,
    deleteEntity: PropTypes.func.isRequired,
    enterTimetableEditor: PropTypes.func,
    entities: PropTypes.array,
    feedSource: PropTypes.object,
    newGtfsEntity: PropTypes.func.isRequired,
    setActiveEntity: PropTypes.func.isRequired,
    updateActiveEntity: PropTypes.func.isRequired,
    width: PropTypes.number.isRequired
  }

  state = {}

  _onResize = () => {
    this.setState({width: window.innerWidth, height: window.innerHeight})
  }

  componentWillMount () {
    this._onResize()
  }

  componentDidMount () {
    window.addEventListener('resize', this._onResize)
  }

  componentWillUnmount () {
    window.removeEventListener('resize', this._onResize)
  }

  componentWillReceiveProps (nextProps) {
    let fromIndex, toIndex
    if (nextProps.activeComponent !== this.props.activeComponent) {
      this.setState({fromIndex, toIndex})
    }
  }

  shouldComponentUpdate (nextProps) {
    // simply running shallowEqual on all props does not give us the performance we need here
    // (especially with many, many stops)
    return !shallowEqual(nextProps.entities, this.props.entities) ||
    !shallowEqual(nextProps.activeEntity, this.props.activeEntity) ||
    !shallowEqual(nextProps.activeEntityId, this.props.activeEntityId) ||
    !shallowEqual(nextProps.activeComponent, this.props.activeComponent) ||
    !shallowEqual(nextProps.feedSource, this.props.feedSource)
  }

  _optionRenderer = ({ focusedOption, focusedOptionIndex, focusOption, key, labelKey, option, options, selectValue, style, valueArray }) => {
    const className = ['VirtualizedSelectOption']
    if (option === focusedOption) {
      className.push('VirtualizedSelectFocusedOption')
    }
    if (option.disabled) {
      className.push('VirtualizedSelectDisabledOption')
    }
    const events = option.disabled
      ? {}
      : {
        onClick: () => selectValue(option),
        onMouseOver: () => focusOption(option)
      }
    return (

          {option.label}

    )
  }

  _getRowStyle (index, list) {
    const activeColor = '#F2F2F2'
    const rowStyle = {
      borderBottom: 'solid 1px #ddd',
      cursor: 'pointer',
      outline: 'none'
    }
    if (list[index] && (list[index].isActive || list[index].isSelected)) {
      rowStyle.backgroundColor = activeColor
    }
    return rowStyle
  }

  updateIndexes = (fromIndex, toIndex) => {
    this.setState({fromIndex, toIndex})
  }

  _onClickTimetableEditor = () => this.props.enterTimetableEditor()

  _onClickNew = () => this.props.newGtfsEntity(this.props.feedSource.id, this.props.activeComponent)

  _onRowClick (index, list, shiftKey) {
    let fromIndex, toIndex
    if (shiftKey && this.props.activeEntity && !list[index].isActive) {
      const selectedIndex = list.findIndex(e => e.id === this.props.activeEntity.id)
      fromIndex = selectedIndex > index ? index : selectedIndex
      toIndex = selectedIndex < index ? index : selectedIndex
      console.log(`select multiple from ${fromIndex} to ${toIndex}`)
      this.props.setActiveEntity(this.props.feedSource.id, this.props.activeComponent)
    } else
    if (list[index].isActive) {
      this.props.setActiveEntity(this.props.feedSource.id, this.props.activeComponent)
    } else {
      this.props.setActiveEntity(this.props.feedSource.id, this.props.activeComponent, list[index])
    }
    this.setState({fromIndex, toIndex})
  }
  // TODO: add hover to row rendering
  // _rowRenderer (props) {
  //
  // }
  render () {
    const {
      tableView,
      width,
      activeEntity,
      activeComponent,
      hasRoutes,
      feedSource,
      entities
    } = this.props
    const sidePadding = '5px'
    const panelWidth = !tableView ? `${width}px` : '100%'
    const panelStyle = {
      width: panelWidth,
      height: '100%',
      position: 'absolute',
      left: '0px',
      zIndex: 1,
      backgroundColor: 'white',
      paddingRight: '0px',
      paddingLeft: sidePadding
    }
    let activeIndex
    const list = entities && entities.length
      ? entities.map((entity, index) => {
        if (activeEntity && entity.id === activeEntity.id) {
          activeIndex = index
        }
        const isActive = activeEntity && entity.id === activeEntity.id
        const isSelected = typeof this.state.fromIndex !== 'undefined' && typeof this.state.toIndex !== 'undefined' && index >= this.state.fromIndex && index <= this.state.toIndex
        const name = getEntityName(activeComponent, entity) || '[Unnamed]'
        return {name, id: entity.id, isActive, isSelected}
      }
    )
    : []
    const entityList = list.length
    ? <Table
      width={width - 5}
      height={560}
      key={`${feedSource.id}-${activeComponent}-table`}
      disableHeader
      headerHeight={20}
      rowHeight={25}
      scrollToIndex={activeIndex}
      rowClassName='noselect'
      rowStyle={({index}) => this._getRowStyle(index, list)}
      rowCount={list.length}
      onRowClick={({event, index}) => {
        this._onRowClick(index, list, event.shiftKey)
      }}
      rowGetter={({index}) => list[index]}
      >
      <Column
        label='Name'
        dataKey='name'
        className='small entity-list-row'
        style={{outline: 'none'}}
        width={width - 5}
      />

      :

        <Button
          bsSize='small'
          disabled={entities && entities.findIndex(e => e.id === 'new') !== -1}
          onClick={this._onClickNew}>
          <Icon type='plus' /> Create first {activeComponent === 'scheduleexception' ? 'exception' : activeComponent}
        </Button>

    return (

          <EntityListButtons
            fromIndex={this.state.fromIndex}
            toIndex={this.state.toIndex}
            list={list}
            updateIndexes={this.updateIndexes}
            {...this.props}
          />
          {activeComponent === 'calendar' || activeComponent === 'scheduleexception'
            ? <Button
              style={{marginTop: '10px'}}
              block
              disabled={!hasRoutes}
              onClick={this._onClickTimetableEditor}
            >
              <Icon type='pencil' /> Edit schedules
            </Button>
            : null
          }
          {/* Table view button */}

        <EntityListSecondaryActions {...this.props} />
        {!tableView
          ? entityList
          : null // GtfsTable fully removed from repo, last available at fe29528569f5f64c23a49d2af0bd224f3d63d010
        }

    )
  }
}
<|endoftext|>


<commit_before>$(document).ready(function() {
  console.log('main.js is ready');

  // =======================================
  // ========== HELP METHODS ZONE ==========
  // =======================================

  // This function helps to use Bootstrap Notify
  var myNotify = function(message, type) {
      $.notify({message}, {type, placement: {from: 'bottom'}});
  };

  // This function helps to show data in the table from the API GET request
  var showTable = function(res) {
    console.log(res);
    var newHTML = '';
    if (res.ITEMS.length == 0) {
        newHTML = [];
        myNotify(`No entries was found. Try another request.`, 'warning');
    } else {
        newHTML = $.map(res.ITEMS, function(value) {
            return `

                                ${value.sum}
                                NZ

DATE: ${moment(value.date).format('L')}
                                ${value.category}

${value.comment}

            `;
        });
    }
    $("#main-table").html(newHTML.join(""));
  };

  // =======================================
  // ============ NEW ENTRY ZONE ===========
  // =======================================

  $('#main-form').validator().on('submit', function(e) {
    var formData = $('form').serializeArray();
    // console.log(formData);
    if (e.isDefaultPrevented()) {
        // handle the invalid form...
        myNotify(`Form is not completed OR you made some mistake!`, 'danger');
        } else {
        // everything looks good!
        $.ajax({
            type: 'POST',
            url: '/api/entries',
            headers: { "X-CSRF-Token": $('input[name="_csrf"]')[0].value },
            dataType: "json",
            data: {
                user      : $('input[name="_userID"]')[0].value,
                date      : formData[2].value,
                sum       : formData[3].value,
                category  : formData[4].value,
                comment   : formData[5].value
            },
            success: function(data) {
                myNotify('New entry posted successfuly!', 'success');
                showALLEntries();
                console.log(data);
            },
            error: function(err) {
                myNotify(`${err.responseText} - ${err.statusText}`, 'danger');
                console.log(err);
            }
        });
        e.preventDefault();
      }
  });

  // =======================================
  // ========== API REQUESTS ZONE ==========
  // =======================================

  // AJAX GET ALL ENTRIES request
  var showALLEntries = function() {
      $.ajax({
          type: 'GET',
          url: '/api/entries',
          success: function(data) {
              showTable(data)
          },
          error: function(err){
              myNotify(`${err.status} - ${err.statusText}`, 'danger');
              console.log(err);
          }
      });
  };

  // =======================================
  // ============= BUTTONS =================
  // =======================================

  // SHOW ALL ENTRIES BUTTON
  $('#show-all-button').click(function() {
      var filter = null;
      if ($('#category-selector')[0].value != 'All entries') {
          filter = $('#category-selector')[0].value;
      }
      $.ajax({
          type  : 'GET',
          url   : '/api/entries/',
          data  : { category: filter },
          success: function(data) {
              showTable(data);
          },
          error: function(err) {
              myNotify(`${err.status} - ${err.statusText}`, 'danger');
              console.log(err);
          }
      });
  });

  // RESET CATEGODY BUTTON
  $('#category-reset-button').click(function() {
      $('#category-selector')[0].value = 'All entries';
      showALLEntries();
  });

  // COPY ELEMENT ID TO THE EDIT FORM
  $(document).on('click', '.entries-list', function() {
      $('#inputId')[0].value = $(this)[0].id;
      return false;
  });

  // GET entry by ID
  $('#show-id-button').click(function() {
      $.ajax({
          type: 'GET',
          url: '/api/entry/' + $('#inputId')[0].value,
          success: function(data) {
              showTable(data);
          },
          error: function(err) {
              myNotify(`${err.status} - ${err.statusText}`, 'danger');
              console.log(err);
          }
      });
  });

  // DELETE entry by ID
  $('#delete-id-button').click(function() {
      $.ajax({
          type: 'DELETE',
          url: '/api/entry/' + $('#inputId')[0].value,
          success: function(response) {
              myNotify(`Enrtry ID: ${$('#inputId')[0].value} deleted successfuly`, 'warning');
              $('#inputId')[0].value = '';
              showALLEntries();
          },
          error: function(err) {
              myNotify(`${err.status} - ${err.statusText}`, 'danger');
              console.log(err);
          }
      });
  });

  // POST NEW ENTRY BUTTON TEST
  $('#post-entry-button').click(function() {
      $.ajax({
          type: 'POST',
          url: '/api/entries',
          headers: { "X-CSRF-Token": $('input[type="hidden"]')[0].value },
          dataType: "json",
          data: {
              user      : $('input[name="_userID"]')[0].value,
              date      : new Date(),
              sum       : 20.1,
              category  : "Home",
              comment   : "POST request from AJAX"
          },
          success: function(data) {
              myNotify('New entry posted successfuly!', 'success');
              showALLEntries();
              console.log(data);
          },
          error: function(err) {
              myNotify(`${err.status} - ${err.statusText}`, 'danger');
              console.log(err);
          }
      });
  });

  // =======================================
  // ============= UPLOAD CSV ==============
  // =======================================

  // Parse CSV using papaparse.min.js
  $('#upload-input').on('change', function() {
        $('#upload-input').parse({
            config: {
                header: false,
                delimiter: ',',
                complete: function(results, file) {
                    console.log("This file done:", file, results.data);

                    var newTable = [];
                    for (let row of results.data.slice(8)) {
                        if (row[0] == '') continue;
                        newTable.push(`

                            ${row[0]}
                            ${row[4]}
                            ${row[5]}
                            ${row[6]}

                                <select class="form-control" id="selectCategory" name="selectCategory">
                                    <option>Home</option>
                                    <option>Market</option>
                                    <option>Electronics</option>
                                    <option>Gifts</option>
                                    <option>Cafe</option>
                                </select>

                            `);
                    }
                    $("#import-data").html(newTable.join(""));
                }
            },
            complete: function() {
                console.log("All files done!");
            }
        });
    });

    $('.upload-btn').on('click', function (){
        $('#upload-input').click();
        $('.progress-bar').text('0%');
        $('.progress-bar').width('0%');
    });

});
<commit_msg>Fixed new-entry-form ugly BUGGGGGGGG<commit_after>$(document).ready(function() {
  console.log('main.js is ready');

  // =======================================
  // ========== HELP METHODS ZONE ==========
  // =======================================

  // This function helps to use Bootstrap Notify
  var myNotify = function(message, type) {
      $.notify({message}, {type, placement: {from: 'bottom'}});
  };

  // This function helps to show data in the table from the API GET request
  var showTable = function(res) {
    console.log(res);
    var newHTML = '';
    if (res.ITEMS.length == 0) {
        newHTML = [];
        myNotify(`No entries was found. Try another request.`, 'warning');
    } else {
        newHTML = $.map(res.ITEMS, function(value) {
            return `

                                ${value.sum}
                                NZ

DATE: ${moment(value.date).format('L')}
                                ${value.category}

${value.comment}

            `;
        });
    }
    $("#main-table").html(newHTML.join(""));
  };

  // =======================================
  // ============ NEW ENTRY ZONE ===========
  // =======================================

  $('#new-entry-form').validator().on('submit', function(e) {
    var formData = $('form').serializeArray();
    // console.log(formData);
    if (e.isDefaultPrevented()) {
        // handle the invalid form...
        myNotify(`Form is not completed OR you made some mistake!`, 'danger');
        } else {
        // everything looks good!
        $.ajax({
            type: 'POST',
            url: '/api/entries',
            headers: { "X-CSRF-Token": $('input[name="_csrf"]')[0].value },
            dataType: "json",
            data: {
                user      : $('input[name="_userID"]')[0].value,
                date      : formData[2].value,
                sum       : formData[3].value,
                category  : formData[4].value,
                comment   : formData[5].value
            },
            success: function(data) {
                myNotify('New entry posted successfuly!', 'success');
                showALLEntries();
                console.log(data);
            },
            error: function(err) {
                myNotify(`${err.responseText} - ${err.statusText}`, 'danger');
                console.log(err);
            }
        });
        e.preventDefault();
      }
  });

  // =======================================
  // ========== API REQUESTS ZONE ==========
  // =======================================

  // AJAX GET ALL ENTRIES request
  var showALLEntries = function() {
      $.ajax({
          type: 'GET',
          url: '/api/entries',
          success: function(data) {
              showTable(data)
          },
          error: function(err){
              myNotify(`${err.status} - ${err.statusText}`, 'danger');
              console.log(err);
          }
      });
  };

  // =======================================
  // ============= BUTTONS =================
  // =======================================

  // SHOW ALL ENTRIES BUTTON
  $('#show-all-button').click(function() {
      var filter = null;
      if ($('#category-selector')[0].value != 'All entries') {
          filter = $('#category-selector')[0].value;
      }
      $.ajax({
          type  : 'GET',
          url   : '/api/entries/',
          data  : { category: filter },
          success: function(data) {
              showTable(data);
          },
          error: function(err) {
              myNotify(`${err.status} - ${err.statusText}`, 'danger');
              console.log(err);
          }
      });
  });

  // RESET CATEGODY BUTTON
  $('#category-reset-button').click(function() {
      $('#category-selector')[0].value = 'All entries';
      showALLEntries();
  });

  // COPY ELEMENT ID TO THE EDIT FORM
  $(document).on('click', '.entries-list', function() {
      $('#inputId')[0].value = $(this)[0].id;
      return false;
  });

  // GET entry by ID
  $('#show-id-button').click(function() {
      $.ajax({
          type: 'GET',
          url: '/api/entry/' + $('#inputId')[0].value,
          success: function(data) {
              showTable(data);
          },
          error: function(err) {
              myNotify(`${err.status} - ${err.statusText}`, 'danger');
              console.log(err);
          }
      });
  });

  // DELETE entry by ID
  $('#delete-id-button').click(function() {
      $.ajax({
          type: 'DELETE',
          url: '/api/entry/' + $('#inputId')[0].value,
          success: function(response) {
              myNotify(`Enrtry ID: ${$('#inputId')[0].value} deleted successfuly`, 'warning');
              $('#inputId')[0].value = '';
              showALLEntries();
          },
          error: function(err) {
              myNotify(`${err.status} - ${err.statusText}`, 'danger');
              console.log(err);
          }
      });
  });

  // POST NEW ENTRY BUTTON TEST
  $('#post-entry-button').click(function() {
      $.ajax({
          type: 'POST',
          url: '/api/entries',
          headers: { "X-CSRF-Token": $('input[type="hidden"]')[0].value },
          dataType: "json",
          data: {
              user      : $('input[name="_userID"]')[0].value,
              date      : new Date(),
              sum       : 20.1,
              category  : "Home",
              comment   : "POST request from AJAX"
          },
          success: function(data) {
              myNotify('New entry posted successfuly!', 'success');
              showALLEntries();
              console.log(data);
          },
          error: function(err) {
              myNotify(`${err.status} - ${err.statusText}`, 'danger');
              console.log(err);
          }
      });
  });

  // =======================================
  // ============= UPLOAD CSV ==============
  // =======================================

  // Parse CSV using papaparse.min.js
  $('#upload-input').on('change', function() {
        $('#upload-input').parse({
            config: {
                header: false,
                delimiter: ',',
                complete: function(results, file) {
                    var newTable = [];
                    // remove header from the table
                    for (let row of results.data.slice(8)) {
                        // skip emty row
                        if (row[0] == '') continue;
                        newTable.push(`

                            ${row[0]}
                            ${row[4]}
                            ${row[5]}
                            ${row[6]}

                                <select class="form-control" id="selectCategory" name="selectCategory">
                                    <option>Home</option>
                                    <option>Market</option>
                                    <option>Electronics</option>
                                    <option>Gifts</option>
                                    <option>Cafe</option>
                                </select>

                            `);
                    }
                    $("#import-data").html(newTable.join(""));
                }
            },
            complete: function() {
                console.log("All files done!");
            }
        });
    });

    $('.upload-btn').on('click', function (){
        $('#upload-input').click();
        $('.progress-bar').text('0%');
        $('.progress-bar').width('0%');
    });

});
<|endoftext|>


<commit_before>import got from 'got';
import Keyv from 'keyv';
import atob from 'atob';
import cachecontrol from 'parse-cache-control';
import jsonwebtoken from 'jsonwebtoken';
import logger from './logger';
import { core as config } from './conf';

export class JwtAuth {
  constructor() {
    this.cache = new Keyv();
    this.options = {
      audience: config.get('auth.googleClientId') || null,
      issuer: [
        'accounts.google.com',
        'https://accounts.google.com',
      ],
    };
    this.init();
  }

  /**
   * Initializes the cache of KID (key IDs) and PEM mappings from google.
   * It returns a promise of all the keys being added to the cache map. This allows
   * the caller to know when all items will be available to find in the map.
   *
   * @return {Promise} A promise that resolves when all items are in the cache map
   */
  init() {
    logger.info('Updating signing PEMs for JWT verification');
    return got('https://www.googleapis.com/oauth2/v1/certs', { json: true })
      .then(response => {
        const expires = cachecontrol(response.headers['cache-control']);
        if (!expires) {
          logger.error('Unable to parse cache-control header from Google JWT PEM source');
        }
        const ttl = expires['max-age'];
        const kids = response.body;
        logger.debug(`Adding ${Object.keys(kids).length} PEMS with ttl ${ttl}s`);
        const toAdd = Object.keys(kids).map(k => this.cache.set(k, kids[k], ttl * 1000));
        return Promise.all(toAdd);
      });
    // Surely, we need a catch here, but not sure yet what to do about it...
  }

  update(err) {
    const { kid } = err.jwt.header;
    return this.init()
      .then(this.cache.get(kid))
      .catch(error => {
        logger.error('Got error fetching PEMs from Google. JWTs may not validate!', error.message);
        return undefined;
      });
  }

  lookup(jwt) {
    const { kid } = jwt.header;
    // a client passing an old JWT will incur the overhead of re-querying google for the latest
    // keys to make sure we're not lying to the client. This slows the response a bit - which
    // is probably good (throttling bad requests naturally), however this could also lead to a
    // proxy DoS attach on google's key server. I'd bet they're robust, but an enhancement
    // would be to catch that case and deny further lookups for some amount of time
    return this.cache.get(kid)
      .then(pem => {
        if (pem) {
          logger.debug(`cache lookup of ${kid} returned: ${pem.substring(0, 8)}...`);
          const obj = jwt;
          obj.pem = pem;
          return obj;
        }

        logger.debug(`cache lookup of ${kid} failed`);
        const e = new Error(`kid ${kid} not found!`);
        e.jwt = jwt;
        throw e;
        // throw to the next catch()
      })
      .catch(this.update.bind(this))
      .then(pem => {
        if (pem.pem) return pem; // this came from first then(), above
        const obj = jwt;
        obj.pem = pem;
        return obj;
      });
  }

  /**
   * Validates the provided JWT Object (internal to the JwtAuth class),
   * but generally, this calls jsonwebtoken.verify() and wraps logic around the acceptance
   * and existance of other attributes about the JWT.
   *
   * @param  {Object} jwtObj An object that encapsulates the values needed to authenticate
   *  with a JWT
   * @param  {String} jwtObj.token The entire "original" token (Full value following "JWT "
   *  in an Authorization header)
   * @return {Object}  The payload of the JWT, as a parsed JavaScript Object
   */
  validate(jwtObj) {
    const { pem } = jwtObj;
    if (pem === undefined || !pem) throw new Error('PEM not found for validation!');
    const payload = jsonwebtoken.verify(jwtObj.token, pem, this.jwtOpts);
    // verify will throw exception in case of failure, goes down to .catch
    if (config.has('auth.googleDomain')) {
      const domain = config.get('auth.googleDomain');
      this.checkDomain(domain, payload);
    }
    return payload;
  }

  /**
   * Called by validate() to do some extended checks/validation about the domain matching. This
   * check is only available for Google's JWT objects.
   *
   * @param  {String} domain  The domain to be checked. This should come from the apps settings,
   *  and should be "truth" for the check.
   * @param  {Object} payload The parsed payload as a JavaScript object.
   * @param  {String} payload.hd The home domain setting from Google's JWT implementation.
   * @return {Boolean}      Boolean indicating whether the checks pass
   */
  checkDomain(domain, payload) {
    if (domain && !payload.hd) {
      logger.warn(`Domain verification required for ${domain}, but user ${payload.sub} has no domain given.`);
      throw new Error('User has no google domain');
    }
    if (payload.hd !== domain) {
      logger.warn(`User ${payload.sub} is not a member of ${domain}`);
      throw new Error(`User ${payload.sub} is not a member of ${domain}`);
    }
    return true;
  }

  /**
   * Converts the raw Authorization header into a parsed and checked object for passing to further
   * Promises used by this class.
   *
   * @param  {String} authHeader The raw Authorization header from the request.
   * @return {Object}  An internal representation of the JWT for further processing by this class,
   *  contains the raw headers and payload as well as parsed headers and payloads.
   */
  extractJwt(authHeader) {
    const [, token] = authHeader.split(' ');
    if (!token) {
      logger.warn('Invalid Authorization header format');
      return {};
    }
    if ((token.match(/\./g) || []).length !== 2) {
      logger.warn('Improper JWT!');
      return {};
    }
    const [rawHeader, rawPayload, signature] = token.split('.');
    const header = JSON.parse(atob(rawHeader));
    const payload = JSON.parse(atob(rawPayload));
    return {
      token,
      rawHeader,
      header,
      rawPayload,
      payload,
      signature,
    };
  }

  /**
   * Perform complete validation of the JWT from the Authorization header of a request.
   * This will return a promise. When resolved, the request is valid and should be allowed to
   * continue through the middleware chain. When rejected, the request should not be serviced by
   * the application, and a 401 status should (usually) be returned.
   *
   * @param  {string} authHeader The complete value of the requests Authorization header.
   * @return {Promise} A promise resolving when the checking and validation of the request
   * is completed.
   */
  auth(authHeader) {
    const jwt = this.extractJwt(authHeader);
    // becuase this will be taken into the promise scope, we must explicitly bind this to the
    // class object so we can reference it later.
    // no free lunches... who'd have guessed!
    return this.lookup(jwt).bind(this)
      .catch(e => {
        logger.fatal(e);
        return {};
      })
      .then(this.validate.bind(this));
  }
}

export default function () {
  const jwtAuth = new JwtAuth();

  /**
   * The middleware function to be provided to Express.use()
   *
   * @param  {Object}   req  The Express request to be processed.
   * @param  {Object}   res  The Express response object.
   * @param  {Function} next The Express next() function to be called for the next middleware
   * @return {Promise}     A promise resolving to the action of the auth middleware to perform.
   */
  return function middleware(req, res, next) {
    // I hate doing this, but because EventSource doesn't allow setting headers, there's no way
    // for those requests to legitimately authenticate - short of reimplementing EventSource as
    // an XHR request instead. So, we'll just let those requests through. Don't put too much
    // sensitive info in SSE events...
    if (req.accepts('text/event-stream')) {
      return next();
    }
    const authHeader = req.header('Authorization');
    if (!authHeader) return Promise.resolve(res.status(403).send());
    if (!authHeader.match(/^(jwt|bearer) /i)) return Promise.resolve(res.status(403).send());

    return jwtAuth.auth(authHeader)
      .then(payload => {
        logger.info(`AUDIT - ${payload.sub} (${payload.name}) authenticated for ${req.url}`);
        return next();
      })
      .catch(err => {
        logger.warn(`AUDIT - Failed authentication via JWT from ${req.ip}`, err.message);
        return res.status(401).send();
      });
  };
}
<commit_msg>reduce complexity on default<commit_after>import got from 'got';
import Keyv from 'keyv';
import atob from 'atob';
import cachecontrol from 'parse-cache-control';
import jsonwebtoken from 'jsonwebtoken';
import logger from './logger';
import { core as config } from './conf';

export class JwtAuth {
  constructor() {
    this.cache = new Keyv();
    this.options = {
      audience: config.get('auth.googleClientId') || null,
      issuer: [
        'accounts.google.com',
        'https://accounts.google.com',
      ],
    };
    this.init();
  }

  /**
   * Initializes the cache of KID (key IDs) and PEM mappings from google.
   * It returns a promise of all the keys being added to the cache map. This allows
   * the caller to know when all items will be available to find in the map.
   *
   * @return {Promise} A promise that resolves when all items are in the cache map
   */
  init() {
    logger.info('Updating signing PEMs for JWT verification');
    return got('https://www.googleapis.com/oauth2/v1/certs', { json: true })
      .then(response => {
        const expires = cachecontrol(response.headers['cache-control']);
        if (!expires) {
          logger.error('Unable to parse cache-control header from Google JWT PEM source');
        }
        const ttl = expires['max-age'];
        const kids = response.body;
        logger.debug(`Adding ${Object.keys(kids).length} PEMS with ttl ${ttl}s`);
        const toAdd = Object.keys(kids).map(k => this.cache.set(k, kids[k], ttl * 1000));
        return Promise.all(toAdd);
      });
    // Surely, we need a catch here, but not sure yet what to do about it...
  }

  update(err) {
    const { kid } = err.jwt.header;
    return this.init()
      .then(this.cache.get(kid))
      .catch(error => {
        logger.error('Got error fetching PEMs from Google. JWTs may not validate!', error.message);
        return undefined;
      });
  }

  lookup(jwt) {
    const { kid } = jwt.header;
    // a client passing an old JWT will incur the overhead of re-querying google for the latest
    // keys to make sure we're not lying to the client. This slows the response a bit - which
    // is probably good (throttling bad requests naturally), however this could also lead to a
    // proxy DoS attach on google's key server. I'd bet they're robust, but an enhancement
    // would be to catch that case and deny further lookups for some amount of time
    return this.cache.get(kid)
      .then(pem => {
        if (pem) {
          logger.debug(`cache lookup of ${kid} returned: ${pem.substring(0, 8)}...`);
          const obj = jwt;
          obj.pem = pem;
          return obj;
        }

        logger.debug(`cache lookup of ${kid} failed`);
        const e = new Error(`kid ${kid} not found!`);
        e.jwt = jwt;
        throw e;
        // throw to the next catch()
      })
      .catch(this.update.bind(this))
      .then(pem => {
        if (pem.pem) return pem; // this came from first then(), above
        const obj = jwt;
        obj.pem = pem;
        return obj;
      });
  }

  /**
   * Validates the provided JWT Object (internal to the JwtAuth class),
   * but generally, this calls jsonwebtoken.verify() and wraps logic around the acceptance
   * and existance of other attributes about the JWT.
   *
   * @param  {Object} jwtObj An object that encapsulates the values needed to authenticate
   *  with a JWT
   * @param  {String} jwtObj.token The entire "original" token (Full value following "JWT "
   *  in an Authorization header)
   * @return {Object}  The payload of the JWT, as a parsed JavaScript Object
   */
  validate(jwtObj) {
    const { pem } = jwtObj;
    if (pem === undefined || !pem) throw new Error('PEM not found for validation!');
    const payload = jsonwebtoken.verify(jwtObj.token, pem, this.jwtOpts);
    // verify will throw exception in case of failure, goes down to .catch
    if (config.has('auth.googleDomain')) {
      const domain = config.get('auth.googleDomain');
      this.checkDomain(domain, payload);
    }
    return payload;
  }

  /**
   * Called by validate() to do some extended checks/validation about the domain matching. This
   * check is only available for Google's JWT objects.
   *
   * @param  {String} domain  The domain to be checked. This should come from the apps settings,
   *  and should be "truth" for the check.
   * @param  {Object} payload The parsed payload as a JavaScript object.
   * @param  {String} payload.hd The home domain setting from Google's JWT implementation.
   * @return {Boolean}      Boolean indicating whether the checks pass
   */
  checkDomain(domain, payload) {
    if (domain && !payload.hd) {
      logger.warn(`Domain verification required for ${domain}, but user ${payload.sub} has no domain given.`);
      throw new Error('User has no google domain');
    }
    if (payload.hd !== domain) {
      logger.warn(`User ${payload.sub} is not a member of ${domain}`);
      throw new Error(`User ${payload.sub} is not a member of ${domain}`);
    }
    return true;
  }

  /**
   * Converts the raw Authorization header into a parsed and checked object for passing to further
   * Promises used by this class.
   *
   * @param  {String} authHeader The raw Authorization header from the request.
   * @return {Object}  An internal representation of the JWT for further processing by this class,
   *  contains the raw headers and payload as well as parsed headers and payloads.
   */
  extractJwt(authHeader) {
    const [, token] = authHeader.split(' ');
    if (!token) {
      logger.warn('Invalid Authorization header format');
      return {};
    }
    if ((token.match(/\./g) || []).length !== 2) {
      logger.warn('Improper JWT!');
      return {};
    }
    const [rawHeader, rawPayload, signature] = token.split('.');
    const header = JSON.parse(atob(rawHeader));
    const payload = JSON.parse(atob(rawPayload));
    return {
      token,
      rawHeader,
      header,
      rawPayload,
      payload,
      signature,
    };
  }

  checkAuthHeader(header) {
    return header && header.match(/^(jwt|bearer) /i);
  }

  /**
   * Perform complete validation of the JWT from the Authorization header of a request.
   * This will return a promise. When resolved, the request is valid and should be allowed to
   * continue through the middleware chain. When rejected, the request should not be serviced by
   * the application, and a 401 status should (usually) be returned.
   *
   * @param  {string} authHeader The complete value of the requests Authorization header.
   * @return {Promise} A promise resolving when the checking and validation of the request
   * is completed.
   */
  auth(authHeader) {
    const jwt = this.extractJwt(authHeader);
    // becuase this will be taken into the promise scope, we must explicitly bind this to the
    // class object so we can reference it later.
    // no free lunches... who'd have guessed!
    return this.lookup(jwt).bind(this)
      .catch(e => {
        logger.fatal(e);
        return {};
      })
      .then(this.validate.bind(this));
  }
}

export default function () {
  const jwtAuth = new JwtAuth();

  /**
   * The middleware function to be provided to Express.use()
   *
   * @param  {Object}   req  The Express request to be processed.
   * @param  {Object}   res  The Express response object.
   * @param  {Function} next The Express next() function to be called for the next middleware
   * @return {Promise}     A promise resolving to the action of the auth middleware to perform.
   */
  return function middleware(req, res, next) {
    // I hate doing this, but because EventSource doesn't allow setting headers, there's no way
    // for those requests to legitimately authenticate - short of reimplementing EventSource as
    // an XHR request instead. So, we'll just let those requests through. Don't put too much
    // sensitive info in SSE events...
    if (req.accepts('text/event-stream')) return next();

    const authHeader = req.header('Authorization');
    if (!jwtAuth.checkAuthHeader(authHeader)) return Promise.resolve(res.status(403).send());

    return jwtAuth.auth(authHeader)
      .then(payload => {
        logger.info(`AUDIT - ${payload.sub} (${payload.name}) authenticated for ${req.url}`);
        return next();
      })
      .catch(err => {
        logger.warn(`AUDIT - Failed authentication via JWT from ${req.ip}`, err.message);
        return res.status(401).send();
      });
  };
}
<|endoftext|>


<commit_before>import {Store} from "next-session"

import omit from "lodash/omit"

import db from "server/lib/db/connection"

class SequlizeStore extends Store {
  /**
   * @param {import("server/model/Session").default} model
   */
  constructor(model) {
    super()

    this._session = model
  }

  get = id => db.transaction(transaction => this._session.findByPk(id, {
    transaction, raw: true
  }))

  set = (id, {cookie, ...data}) => db.transaction(async transaction => {
    console.log(cookie.cookieOptions)
    await this._session.upsert({...data, id, cookie: cookie.cookieOptions}, {transaction})
  })

  touch = (id, data) => db.transaction(async transaction => {
    const session = await this._session.findByPk(id, {transaction})

    if (!session) {
      return undefined
    }

    await session.update(omit(data, "id"), {transaction})
  })

  destroy = id => db.transaction(async transaction => {
    await this._session.destroy({transaction, where: {id}})
  })
}

export default SequlizeStore
<commit_msg>Small fix<commit_after>import {Store} from "next-session"

import omit from "lodash/omit"

import db from "server/lib/db/connection"

class SequlizeStore extends Store {
  /**
   * @param {import("server/model/Session").default} model
   */
  constructor(model) {
    super()

    this._session = model
  }

  get = id => db.transaction(transaction => this._session.findByPk(id, {
    transaction, raw: true
  }))

  set = (id, {cookie, ...data}) => db.transaction(async transaction => {
    await this._session.upsert({
      ...data, id, cookie: cookie.cookieOptions
    }, {transaction})
  })

  touch = (id, data) => db.transaction(async transaction => {
    const session = await this._session.findByPk(id, {transaction})

    if (!session) {
      return undefined
    }

    await session.update(omit(data, "id"), {transaction})
  })

  destroy = id => db.transaction(async transaction => {
    await this._session.destroy({transaction, where: {id}})
  })
}

export default SequlizeStore
<|endoftext|>


<commit_before>
import Future from 'fibers/future';
import moment from 'moment';
import { Random } from 'meteor/random';

// worth extending mongo.collection, or just not use it?
// https://stackoverflow.com/questions/34979661/extending-mongo-collection-breaks-find-method
// decided not to use it. If Mongo is desired in future, this collection object can be used to extend and wrap it

API.collection = function (opts) {
  if (typeof opts === 'string') opts = {type:opts};
  if (opts.index === undefined && opts.type !== undefined) opts.index = API.settings.es.index ? API.settings.es.index : (API.settings.name ? API.settings.name : 'noddy');
  this._index = opts.index;
  this._type = opts.type;
  this._history = opts.history;
  this._replicate = opts.replicate;
  this._route = '/' + opts.index + '/';
  if (opts.type) this._route += opts.type + '/';
  API.es.map(this._index,this._type); // only has effect if no mapping already
};

API.collection.prototype.map = function (mapping) {
  return API.es.map(this._index,this._type,mapping); // would overwrite any existing mapping
};

API.collection.prototype.insert = function (uid,obj,refresh) {
  if (typeof uid === 'string' && typeof obj === 'object') {
    obj._id = uid;
  } else if (typeof uid === 'object' && obj === undefined) {
    obj = uid;
  }
  if (!obj.createdAt) {
    obj.createdAt = Date.now();
    obj.created_date = moment(obj.createdAt,"x").format("YYYY-MM-DD HHmm");
  }
  var r = this._route;
  if (!obj._id) obj._id = Random.id;
  r += obj._id;
  return API.es.call('POST',r, obj,refresh);
};

API.collection.prototype.update = function (qry,obj,refresh) {
  var rec;
  if (typeof qry === 'string' && qry.indexOf(' ') === -1) {
    var check = API.es.call('GET',this._route + qry);
    if (check.found !== false) rec = check;
  }
  delete obj._id; // just in case, can't replace the object ID.
  var rt = this._route;
  var update = function(doc) {
    for ( var k in obj ) {
      var upd = doc;
      var parts = k.split('.');
      var last = parts.pop();
      for ( var p in parts ) upd = upd[parts[p]];
      upd[last] = obj[k];
    }
    doc.updatedAt = Date.now();
    doc.updated_date = moment(obj.createdAt,"x").format("YYYY-MM-DD HHmm");
    API.es.call('POST',rt + doc._id, doc, refresh);
    return doc;
  }
  if (rec) {
    return update(rec);
  } else {
    return this.each(qry,update);
  }
};

API.collection.prototype.remove = function(qry) {
  if (typeof qry === 'number' || ( typeof qry === 'string' && qry.indexOf(' ') === -1 ) ) {
    var check = API.es.call('GET',this._route + qry);
    if (check.found !== false) {
      API.es.call('DELETE',this._route + qry);
      return true;
    }
  }
  var rt = this._route;
  var remove = function(doc) {
    API.es.call('DELETE',rt + doc._id);
  }
  return this.each(qry,remove);
}

API.collection.prototype.search = function(qry,qp,find) {
  if (find) {
    if (typeof find === 'object') {
      qp = {q:''}
      for ( var o in find ) {
        qp.q += qp.q.length === 0 ? '' : ' AND ';
        qp.q += o + ':' + (typeof find[o] === 'string' && find[o].indexOf(' ') !== -1 ? '"' + find[o] + '"' : find[o]);
      }
    } else {
      qp = {q:find}
    }
  }
  if (qp) {
    var rt = this._route + '/_search?';
    for ( var op in qp ) rt += op + '=' + qp[op] + '&';
    var data;
    if ( qry && JSON.stringify(qry).length > 2 ) data = qry;
    return API.es.call('GET',rt,data);
  } else if (qry && JSON.stringify(qry).length > 2) {
    return API.es.call('POST',this._route + '/_search',qry);
  } else {
    return API.es.call('GET',this._route + '/_search');
  }
}

API.collection.prototype.count = function(qry,qp,find) {
  try {
    var res = this.search(qry,qp,find);
    return res.hits.total;
  } catch(err) {
    return 0;
  }
}

API.collection.prototype.find = function(q) {
  if (typeof q === 'number' || ( typeof q === 'string' && q.indexOf(' ') === -1 ) ) {
    var check = API.es.call('GET',this._route + q);
    if (check.found !== false) return check._source;
  }
  try {
    var res = this.search(undefined,undefined,q).hits.hits[0];
    return res._source ? res._source : res.fields;
  } catch(err) {
    return false;
  }
}

API.collection.prototype.each = function(q,fn) {
  // TODO could use es.scroll here...
  var res = this.search(undefined,undefined,q);
  var count = res.hits.total;
  var counter = 0;
  while (counter < count) {
    for ( var r in res.hits.hits ) {
      var rec = res.hits.hits[r]._source ? res.hits.hits[r]._source : res.hits.hits[r].fields;
      fn(rec);
    }
    counter += res.hits.hits.length;
    if (counter < count) res = this.search(undefined,undefined,q); // TODO how to pass params like size and from?
  }
  return counter;
}

API.collection.test = function() {
  var result = {passed:true};
  var tc = new API.collection({index:API.settings.es.index + '_test',type:'collection'});
  var recs = [
    {_id:1,hello:'world'},
    {_id:2,goodbye:'world'},
    {goodbye:'world',hello:'sunshine'},
    {goodbye:'marianne',hello:'sunshine'}
  ];
  result.recs = recs;
  for ( var r in recs ) tc.insert(recs[r]);
  var future = new Future();
  setTimeout(function() { future.return(); }, 999);
  future.wait();
  result.count = tc.count();
  result.passed = result.count === recs.length;
  result.search = tc.search();
  result.stringSearch = tc.search(undefined,undefined,'goodbye:"marianne"');
  result.passed = result.stringSearch.hits.total === 1;
  result.objectSearch = tc.search(undefined,undefined,{hello:'sunshine'});
  result.passed = result.objectSearch.hits.total === 2;
  result.idFind = tc.find(1);
  result.passed = typeof result.idFind === 'object';
  result.strFind = tc.find('goodbye:"marianne"');
  result.passed = typeof result.strFind === 'object';
  result.objFind = tc.find({goodbye:'marianne'});
  result.passed = typeof result.objFind === 'object';
  result.objFindMulti = tc.find({goodbye:'world'});
  result.passed = typeof result.objFindMulti === 'object';
  result.each = tc.each('goodbye:"world"',function() { return; });
  result.passed = result.each === 2;
  result.update = tc.update({hello:'world'},{goodbye:'world'});
  future = new Future();
  setTimeout(function() { future.return(); }, 999);
  future.wait();
  result.passed = typeof result.update === 'object' && result.update.goodbye === 'world';
  result.retrieveUpdated = tc.find({hello:'world'});
  result.passed = typeof result.retrieveUpdated === 'object' && result.retrieveUpdated.goodbye === 'world';
  result.goodbyes = tc.count('goodbye:"world"');
  result.passed = result.goodbyes === 3;
  result.remove1 = tc.remove(1);
  future = new Future();
  setTimeout(function() { future.return(); }, 999);
  future.wait();
  result.passed = result.remove1 === true;
  result.helloWorlds = tc.count({hello:'world'});
  result.passed = result.helloWorlds === 0;
  result.remove2 = tc.remove({hello:'sunshine'});
  future = new Future();
  setTimeout(function() { future.return(); }, 999);
  future.wait();
  result.passed = result.remove2 === 2;
  result.remaining = tc.count();
  result.passed = result.remaining === 2;
  return result;
}

<commit_msg>tests update<commit_after>
import Future from 'fibers/future';
import moment from 'moment';
import { Random } from 'meteor/random';

// worth extending mongo.collection, or just not use it?
// https://stackoverflow.com/questions/34979661/extending-mongo-collection-breaks-find-method
// decided not to use it. If Mongo is desired in future, this collection object can be used to extend and wrap it

API.collection = function (opts) {
  if (typeof opts === 'string') opts = {type:opts};
  if (opts.index === undefined && opts.type !== undefined) opts.index = API.settings.es.index ? API.settings.es.index : (API.settings.name ? API.settings.name : 'noddy');
  this._index = opts.index;
  this._type = opts.type;
  this._history = opts.history;
  this._replicate = opts.replicate;
  this._route = '/' + opts.index + '/';
  if (opts.type) this._route += opts.type + '/';
  API.es.map(this._index,this._type); // only has effect if no mapping already
};

API.collection.prototype.map = function (mapping) {
  return API.es.map(this._index,this._type,mapping); // would overwrite any existing mapping
};

API.collection.prototype.insert = function (uid,obj,refresh) {
  if (typeof uid === 'string' && typeof obj === 'object') {
    obj._id = uid;
  } else if (typeof uid === 'object' && obj === undefined) {
    obj = uid;
  }
  if (!obj.createdAt) {
    obj.createdAt = Date.now();
    obj.created_date = moment(obj.createdAt,"x").format("YYYY-MM-DD HHmm");
  }
  var r = this._route;
  if (!obj._id) obj._id = Random.id();
  r += obj._id;
  return API.es.call('POST',r, obj,refresh);
};

API.collection.prototype.update = function (qry,obj,refresh) {
  var rec;
  if (typeof qry === 'string' && qry.indexOf(' ') === -1) {
    var check = API.es.call('GET',this._route + qry);
    if (check.found !== false) rec = check;
  }
  delete obj._id; // just in case, can't replace the object ID.
  var rt = this._route;
  var update = function(doc) {
    for ( var k in obj ) {
      var upd = doc;
      var parts = k.split('.');
      var last = parts.pop();
      for ( var p in parts ) upd = upd[parts[p]];
      upd[last] = obj[k];
    }
    doc.updatedAt = Date.now();
    doc.updated_date = moment(obj.createdAt,"x").format("YYYY-MM-DD HHmm");
    API.es.call('POST',rt + doc._id, doc, refresh);
    return doc;
  }
  if (rec) {
    return update(rec);
  } else {
    return this.each(qry,update);
  }
};

API.collection.prototype.remove = function(qry) {
  if (typeof qry === 'number' || ( typeof qry === 'string' && qry.indexOf(' ') === -1 ) ) {
    var check = API.es.call('GET',this._route + qry);
    if (check.found !== false) {
      API.es.call('DELETE',this._route + qry);
      return true;
    }
  }
  var rt = this._route;
  var remove = function(doc) {
    API.es.call('DELETE',rt + doc._id);
  }
  return this.each(qry,remove);
}

API.collection.prototype.search = function(qry,qp,find) {
  if (find) {
    if (typeof find === 'object') {
      qp = {q:''}
      for ( var o in find ) {
        qp.q += qp.q.length === 0 ? '' : ' AND ';
        qp.q += o + ':' + (typeof find[o] === 'string' && find[o].indexOf(' ') !== -1 ? '"' + find[o] + '"' : find[o]);
      }
    } else {
      qp = {q:find}
    }
  }
  if (qp) {
    var rt = this._route + '/_search?';
    for ( var op in qp ) rt += op + '=' + qp[op] + '&';
    var data;
    if ( qry && JSON.stringify(qry).length > 2 ) data = qry;
    return API.es.call('GET',rt,data);
  } else if (qry && JSON.stringify(qry).length > 2) {
    return API.es.call('POST',this._route + '/_search',qry);
  } else {
    return API.es.call('GET',this._route + '/_search');
  }
}

API.collection.prototype.count = function(qry,qp,find) {
  try {
    var res = this.search(qry,qp,find);
    return res.hits.total;
  } catch(err) {
    return 0;
  }
}

API.collection.prototype.find = function(q) {
  if (typeof q === 'number' || ( typeof q === 'string' && q.indexOf(' ') === -1 ) ) {
    var check = API.es.call('GET',this._route + q);
    if (check.found !== false) return check._source;
  }
  try {
    var res = this.search(undefined,undefined,q).hits.hits[0];
    return res._source ? res._source : res.fields;
  } catch(err) {
    return false;
  }
}

API.collection.prototype.each = function(q,fn) {
  // TODO could use es.scroll here...
  var res = this.search(undefined,undefined,q);
  var count = res.hits.total;
  var counter = 0;
  while (counter < count) {
    for ( var r in res.hits.hits ) {
      var rec = res.hits.hits[r]._source ? res.hits.hits[r]._source : res.hits.hits[r].fields;
      fn(rec);
    }
    counter += res.hits.hits.length;
    if (counter < count) res = this.search(undefined,undefined,q); // TODO how to pass params like size and from?
  }
  return counter;
}

API.collection.test = function() {
  var result = {passed:true};
  var tc = new API.collection({index:API.settings.es.index + '_test',type:'collection'});
  var recs = [
    {_id:1,hello:'world'},
    {_id:2,goodbye:'world'},
    {goodbye:'world',hello:'sunshine'},
    {goodbye:'marianne',hello:'sunshine'}
  ];
  result.recs = recs;
  for ( var r in recs ) tc.insert(recs[r]);
  var future = new Future();
  setTimeout(function() { future.return(); }, 999);
  future.wait();
  result.count = tc.count();
  result.passed = result.count === recs.length;
  result.search = tc.search();
  result.stringSearch = tc.search(undefined,undefined,'goodbye:"marianne"');
  result.passed = result.stringSearch.hits.total === 1;
  result.objectSearch = tc.search(undefined,undefined,{hello:'sunshine'});
  result.passed = result.objectSearch.hits.total === 2;
  result.idFind = tc.find(1);
  result.passed = typeof result.idFind === 'object';
  result.strFind = tc.find('goodbye:"marianne"');
  result.passed = typeof result.strFind === 'object';
  result.objFind = tc.find({goodbye:'marianne'});
  result.passed = typeof result.objFind === 'object';
  result.objFindMulti = tc.find({goodbye:'world'});
  result.passed = typeof result.objFindMulti === 'object';
  result.each = tc.each('goodbye:"world"',function() { return; });
  result.passed = result.each === 2;
  result.update = tc.update({hello:'world'},{goodbye:'world'});
  future = new Future();
  setTimeout(function() { future.return(); }, 999);
  future.wait();
  result.passed = typeof result.update === 'object' && result.update.goodbye === 'world';
  result.retrieveUpdated = tc.find({hello:'world'});
  result.passed = typeof result.retrieveUpdated === 'object' && result.retrieveUpdated.goodbye === 'world';
  result.goodbyes = tc.count('goodbye:"world"');
  result.passed = result.goodbyes === 3;
  result.remove1 = tc.remove(1);
  future = new Future();
  setTimeout(function() { future.return(); }, 999);
  future.wait();
  result.passed = result.remove1 === true;
  result.helloWorlds = tc.count({hello:'world'});
  result.passed = result.helloWorlds === 0;
  result.remove2 = tc.remove({hello:'sunshine'});
  future = new Future();
  setTimeout(function() { future.return(); }, 999);
  future.wait();
  result.passed = result.remove2 === 2;
  result.remaining = tc.count();
  result.passed = result.remaining === 2;
  return result;
}

<|endoftext|>


<commit_before>
var config = require('./config').config;

var fs = require('fs');

exports.handler = (function() {
  var url=require('url'),
    crypto=require('crypto'),
    tokens = {}, lastModified = {}, contentType = {}, content = {};

  function saveState(name, value) {
    fs.writeFile("server-state/" + name + ".json", JSON.stringify(value), function() { });
  }

  function loadState(name) {
    if(fs.existsSync("server-state/" + name + ".json")) {
      return JSON.parse(fs.readFileSync("server-state/" + name + ".json"));
    } else {
      return {}
    }
  }

  function saveData() {
    if(! fs.existsSync("server-state")) {
      fs.mkdirSync("server-state");
    }
    saveState('tokens', tokens);
    saveState('lastModified', lastModified);
    saveState('contentType', contentType);
    saveState('content', content);
  }

  function loadData() {
    tokens = loadState('tokens');
    lastModified = loadState('lastModified');
    contentType = loadState('contentType');
    content = loadState('content');

    console.log("DATA LOADED", tokens, lastModified, contentType, content);
  }

  function makeScopePaths(userName, scopes) {
    var scopePaths=[];
    for(var i=0; i<scopes.length; i++) {
      var thisScopeParts = scopes[i].split(':');
      if(thisScopeParts[0]=='') {
        scopePaths.push(userName+'/:'+thisScopeParts[1]);
      } else {
        scopePaths.push(userName+'/'+thisScopeParts[0]+'/:'+thisScopeParts[1]);
        scopePaths.push(userName+'/public/'+thisScopeParts[0]+'/:'+thisScopeParts[1]);
      }
    }
    return scopePaths;
  }

  function createInitialTokens() {
    if(! config.initialTokens) {
      return;
    }
    for(var token in config.initialTokens) {
      var scopePaths = makeScopePaths(
        config.defaultUserName, config.initialTokens[token]
      );
      console.log('adding ',scopePaths,' for', token);
      tokens[token] = scopePaths;
    }
  }

  function createToken(userName, scopes, cb) {
    crypto.randomBytes(48, function(ex, buf) {
      var token = buf.toString('hex');
      var scopePaths = makeScopePaths(userName, scopes);
      console.log('createToken ',userName,scopes);
      console.log('adding ',scopePaths,' for',token);
      tokens[token] = scopePaths;
      cb(token);
    });
  }
  function mayRead(authorizationHeader, path) {
    if(authorizationHeader) {
      var scopes = tokens[authorizationHeader.substring('Bearer '.length)];
      if(scopes) {
        for(var i=0; i<scopes.length; i++) {
          var scopeParts = scopes[i].split(':');
          if(path.substring(0, scopeParts[0].length)==scopeParts[0]) {
            return true;
          } else {
            console.log(path.substring(0, scopeParts[0].length)+' != '+ scopeParts[0]);
          }
        }
      }
    } else {
      var pathParts = path.split('/');
      return (pathParts[0]=='' && pathParts[2]=='public' && path.substr(-1) != '/');
    }
  }
  function mayWrite(authorizationHeader, path) {
    if(authorizationHeader) {
      var scopes = tokens[authorizationHeader.substring('Bearer '.length)];
      if(scopes) {
        for(var i=0; i<scopes.length; i++) {
          var scopeParts = scopes[i].split(':');
          if(scopeParts.length==2 && scopeParts[1]=='rw' && path.substring(0, scopeParts[0].length)==scopeParts[0]) {
            return true;
          }
        }
      }
    }
  }
  function writeHead(res, status, origin, timestamp, contentType) {
    var headers = {
      'access-control-allow-origin': (origin?origin:'*'),
      'access-control-allow-headers': 'content-type, authorization, origin',
      'access-control-allow-methods': 'GET, PUT, DELETE',
    };
    if(timestamp) {
      headers['last-modified']= new Date(timestamp).toString();
    }
    if(contentType) {
      headers['content-type']= contentType;
    }
    res.writeHead(status, headers);
  }
  function writeRaw(res, contentType, content, origin, timestamp) {
    console.log('access-control-allow-origin:'+ (origin?origin:'*'));
    console.log(contentType);
    console.log(content);
    writeHead(res, 200, origin, timestamp, contentType);
    res.write(content);
    res.end();
  }
  function writeJson(res, obj, origin, timestamp) {
    writeRaw(res, 'application/json', JSON.stringify(obj), origin, timestamp);
  }
  function writeHtml(res, html) {
    res.writeHead(200, {
      'content-type': 'text/html'
    });
    res.write('<!DOCTYPE html lang="en"><title>'+config.host+'</title><meta charset="utf-8">'+html+'');
    res.end();
  }
  function give404(res, origin) {
    console.log('404');
    console.log(content);
    writeHead(res, 404, origin, 'now');
    res.end();
  }
  function computerSaysNo(res, origin) {
    console.log('COMPUTER_SAYS_NO');
    console.log(tokens);
    writeHead(res, 401, origin, 'now');
    res.end();
  }

  function toHtml(str) {
    return str
      .replace(/&/g, '&')
      .replace(/</g, '<')
      .replace(/>/g, '>')
      .replace(/"/g, '"');
  }
  function portal(urlObj, res) {
    res.writeHead(200, {
      'content-type': 'text/html'
    });
    res.write('<!DOCTYPE html lang="en"><title>'+config.host+'</title><meta charset="utf-8">
');
    var scopes = {
      'http://todomvc.michiel.5apps.com/': ['tasks:rw'],
      'http://litewrite.michiel.5apps.com/': ['documents:rw'],
      'http://docrastinate.michiel.5apps.com/': ['documents:rw'],
      'http://expenses.michiel.5apps.com/': ['money:rw'],
      'http://evanw-continuous-calendar.michiel.5apps.com/': ['calendar:rw'],
      'http://seven20.epic720.5apps.com/manage.htm': [':rw']
    };
    var outstanding = 0;
    for(var i in scopes) {
      outstanding++;
      (function(i) {
        createToken(config.defaultUserName, scopes[i], function(token) {
          res.write('
'+i+'
');
            +'&access_token='+token+'">'+i+'
');
          outstanding--;
          if(outstanding==0) {
            res.write('
');
            res.end();
          }
        });
      })(i);
    }
  }
  function webfinger(urlObj, res) {
    console.log('WEBFINGER');
    if(urlObj.query['resource']) {
      userAddress = urlObj.query['resource'].substring('acct:'.length);
      userName = userAddress.split('@')[0];
    }
    writeJson(res, {
      links:[{
        href: config.protocol+'://'+config.host+'/storage/'+userName,
        rel: "remoteStorage",
        type: "https://www.w3.org/community/rww/wiki/read-write-web-00#simple",
        properties: {
          'auth-method': "https://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2",
          'auth-endpoint': config.protocol+'://'+config.host+'/auth/'+userName
        }
      }]
    });
  }
  function oauth(urlObj, res) {
    console.log('OAUTH');
    var scopes = decodeURIComponent(urlObj.query['scope']).split(' '),
      clientId = decodeURIComponent(urlObj.query['client_id']),
      redirectUri = decodeURIComponent(urlObj.query['redirect_uri']),
      clientIdToMatch,
      userName;
    if(redirectUri.split('://').length<2) {
      clientIdToMatch=redirectUri;
    } else {
      clientIdToMatch = redirectUri.split('://')[1].split('/')[0];
    }
    if(clientId != clientIdToMatch) {
      writeHtml(res, 'we do not trust this combination of client_id and redirect_uri');
    } else {
      var userName = urlObj.pathname.substring('/auth/'.length);
      createToken(userName, scopes, function(token) {
        writeHtml(res, 'Allow');
      });
    }
  }
  function storage(req, urlObj, res) {
    var path=urlObj.pathname.substring('/storage/'.length);
    if(req.method=='OPTIONS') {
      console.log('OPTIONS ', req.headers);
      writeJson(res, null, req.headers.origin);
    } else if(req.method=='GET') {
      console.log('GET');
      if(mayRead(req.headers.authorization, path)) {
        if(content[path]) {
          if(path.substr(-1)=='/') {
            writeJson(res, content[path], req.headers.origin, 0);
          } else {
            writeRaw(res, contentType[path], content[path], req.headers.origin, lastModified[path]);
          }
        } else {
          if(path.substr(-1)=='/') {
            writeJson(res, {}, req.headers.origin, 0);
          } else {
            give404(res, req.headers.origin);
          }
        }
      } else {
        computerSaysNo(res, req.headers.origin);
      }
    } else if(req.method=='PUT') {
      console.log('PUT');
      if(mayWrite(req.headers.authorization, path) && path.substr(-1)!='/') {
        var dataStr = '';
        req.on('data', function(chunk) {
          dataStr+=chunk;
        });
        req.on('end', function(chunk) {
          var timestamp = new Date().getTime();
          content[path]=dataStr;
          contentType[path]=req.headers['content-type'];
          console.log('stored '+path, content[path], contentType[path]);
          lastModified[path]=timestamp;
          saveData();
          var pathParts=path.split('/');
          var timestamp=new Date().getTime();
          console.log(pathParts);
          var fileItself=true;
          while(pathParts.length > 2) {
            var thisPart = pathParts.pop();
            if(fileItself) {
              fileItself=false;
            } else {
              thisPart += '/';
            }
            if(!content[pathParts.join('/')+'/']) {
              content[pathParts.join('/')+'/'] = {};
            }
            content[pathParts.join('/')+'/'][thisPart]=timestamp;
            console.log('stored parent '+pathParts.join('/')+'/ ['+thisPart+']='+timestamp, content[pathParts.join('/')+'/']);
          }
          console.log('content:', content);
          console.log('contentType:', contentType);
          console.log('lastModified:', lastModified);
          writeJson(res, null, req.headers.origin, timestamp);
        });
      } else {
        computerSaysNo(res, req.headers.origin);
      }
    } else if(req.method=='DELETE') {
      console.log('DELETE');
      if(mayWrite(req.headers.authorization, path)) {
          var timestamp = new Date().getTime();
          delete content[path];
          delete contentType[path];
          lastModified[path]=timestamp;
          var pathParts=path.split('/');
          var thisPart = pathParts.pop();
          if(content[pathParts.join('/')+'/']) {
            console.log('delete content['+pathParts.join('/')+'/]['+thisPart+']');
            delete content[pathParts.join('/')+'/'][thisPart];
          }
          console.log(content);
          writeJson(res, null, req.headers.origin, timestamp);
      } else {
        computerSaysNo(res, req.headers.origin);
      }
    } else {
      console.log('ILLEGAL '+req.method);
      computerSaysNo(res, req.headers.origin);
    }
  }
  function serve(req, res, staticsMap) {
    var urlObj = url.parse(req.url, true), userAddress, userName;
    console.log(urlObj);
    if(urlObj.pathname == '/') {
      console.log('PORTAL');
      portal(urlObj, res);
    } else if(urlObj.pathname == '/.well-known/host-meta.json') {//TODO: implement rest of webfinger
      console.log('HOST-META');
      webfinger(urlObj, res);
    } else if(urlObj.pathname.substring(0, '/auth/'.length) == '/auth/') {
      console.log('OAUTH');
      oauth(urlObj, res);
    } else if(urlObj.pathname.substring(0, '/storage/'.length) == '/storage/') {
      console.log('STORAGE');
      storage(req, urlObj, res);
    } else {
      console.log('UNKNOWN');
      writeJson(res, urlObj.query);
    }
  }

  loadData();
  createInitialTokens();

  return {
    serve: serve
  };
})();

if(require.main==module) {//if this file is directly called from the CLI
  require('http').createServer(exports.handler.serve).listen(config.port);
  console.log("Example server started on 0.0.0.0:" + config.port);
}

<commit_msg>example server: also dump data on DELETE<commit_after>
var config = require('./config').config;

var fs = require('fs');

exports.handler = (function() {
  var url=require('url'),
    crypto=require('crypto'),
    tokens = {}, lastModified = {}, contentType = {}, content = {};

  function saveState(name, value) {
    fs.writeFile("server-state/" + name + ".json", JSON.stringify(value), function() { });
  }

  function loadState(name) {
    if(fs.existsSync("server-state/" + name + ".json")) {
      return JSON.parse(fs.readFileSync("server-state/" + name + ".json"));
    } else {
      return {}
    }
  }

  function saveData() {
    if(! fs.existsSync("server-state")) {
      fs.mkdirSync("server-state");
    }
    saveState('tokens', tokens);
    saveState('lastModified', lastModified);
    saveState('contentType', contentType);
    saveState('content', content);
  }

  function loadData() {
    tokens = loadState('tokens');
    lastModified = loadState('lastModified');
    contentType = loadState('contentType');
    content = loadState('content');

    console.log("DATA LOADED", tokens, lastModified, contentType, content);
  }

  function makeScopePaths(userName, scopes) {
    var scopePaths=[];
    for(var i=0; i<scopes.length; i++) {
      var thisScopeParts = scopes[i].split(':');
      if(thisScopeParts[0]=='') {
        scopePaths.push(userName+'/:'+thisScopeParts[1]);
      } else {
        scopePaths.push(userName+'/'+thisScopeParts[0]+'/:'+thisScopeParts[1]);
        scopePaths.push(userName+'/public/'+thisScopeParts[0]+'/:'+thisScopeParts[1]);
      }
    }
    return scopePaths;
  }

  function createInitialTokens() {
    if(! config.initialTokens) {
      return;
    }
    for(var token in config.initialTokens) {
      var scopePaths = makeScopePaths(
        config.defaultUserName, config.initialTokens[token]
      );
      console.log('adding ',scopePaths,' for', token);
      tokens[token] = scopePaths;
    }
  }

  function createToken(userName, scopes, cb) {
    crypto.randomBytes(48, function(ex, buf) {
      var token = buf.toString('hex');
      var scopePaths = makeScopePaths(userName, scopes);
      console.log('createToken ',userName,scopes);
      console.log('adding ',scopePaths,' for',token);
      tokens[token] = scopePaths;
      cb(token);
    });
  }
  function mayRead(authorizationHeader, path) {
    if(authorizationHeader) {
      var scopes = tokens[authorizationHeader.substring('Bearer '.length)];
      if(scopes) {
        for(var i=0; i<scopes.length; i++) {
          var scopeParts = scopes[i].split(':');
          if(path.substring(0, scopeParts[0].length)==scopeParts[0]) {
            return true;
          } else {
            console.log(path.substring(0, scopeParts[0].length)+' != '+ scopeParts[0]);
          }
        }
      }
    } else {
      var pathParts = path.split('/');
      return (pathParts[0]=='' && pathParts[2]=='public' && path.substr(-1) != '/');
    }
  }
  function mayWrite(authorizationHeader, path) {
    if(authorizationHeader) {
      var scopes = tokens[authorizationHeader.substring('Bearer '.length)];
      if(scopes) {
        for(var i=0; i<scopes.length; i++) {
          var scopeParts = scopes[i].split(':');
          if(scopeParts.length==2 && scopeParts[1]=='rw' && path.substring(0, scopeParts[0].length)==scopeParts[0]) {
            return true;
          }
        }
      }
    }
  }
  function writeHead(res, status, origin, timestamp, contentType) {
    var headers = {
      'access-control-allow-origin': (origin?origin:'*'),
      'access-control-allow-headers': 'content-type, authorization, origin',
      'access-control-allow-methods': 'GET, PUT, DELETE',
    };
    if(timestamp) {
      headers['last-modified']= new Date(timestamp).toString();
    }
    if(contentType) {
      headers['content-type']= contentType;
    }
    res.writeHead(status, headers);
  }
  function writeRaw(res, contentType, content, origin, timestamp) {
    console.log('access-control-allow-origin:'+ (origin?origin:'*'));
    console.log(contentType);
    console.log(content);
    writeHead(res, 200, origin, timestamp, contentType);
    res.write(content);
    res.end();
  }
  function writeJson(res, obj, origin, timestamp) {
    writeRaw(res, 'application/json', JSON.stringify(obj), origin, timestamp);
  }
  function writeHtml(res, html) {
    res.writeHead(200, {
      'content-type': 'text/html'
    });
    res.write('<!DOCTYPE html lang="en"><title>'+config.host+'</title><meta charset="utf-8">'+html+'');
    res.end();
  }
  function give404(res, origin) {
    console.log('404');
    console.log(content);
    writeHead(res, 404, origin, 'now');
    res.end();
  }
  function computerSaysNo(res, origin) {
    console.log('COMPUTER_SAYS_NO');
    console.log(tokens);
    writeHead(res, 401, origin, 'now');
    res.end();
  }

  function toHtml(str) {
    return str
      .replace(/&/g, '&')
      .replace(/</g, '<')
      .replace(/>/g, '>')
      .replace(/"/g, '"');
  }
  function portal(urlObj, res) {
    res.writeHead(200, {
      'content-type': 'text/html'
    });
    res.write('<!DOCTYPE html lang="en"><title>'+config.host+'</title><meta charset="utf-8">
');
    var scopes = {
      'http://todomvc.michiel.5apps.com/': ['tasks:rw'],
      'http://litewrite.michiel.5apps.com/': ['documents:rw'],
      'http://docrastinate.michiel.5apps.com/': ['documents:rw'],
      'http://expenses.michiel.5apps.com/': ['money:rw'],
      'http://evanw-continuous-calendar.michiel.5apps.com/': ['calendar:rw'],
      'http://seven20.epic720.5apps.com/manage.htm': [':rw']
    };
    var outstanding = 0;
    for(var i in scopes) {
      outstanding++;
      (function(i) {
        createToken(config.defaultUserName, scopes[i], function(token) {
          res.write('
'+i+'
');
            +'&access_token='+token+'">'+i+'
');
          outstanding--;
          if(outstanding==0) {
            res.write('
');
            res.end();
          }
        });
      })(i);
    }
  }
  function webfinger(urlObj, res) {
    console.log('WEBFINGER');
    if(urlObj.query['resource']) {
      userAddress = urlObj.query['resource'].substring('acct:'.length);
      userName = userAddress.split('@')[0];
    }
    writeJson(res, {
      links:[{
        href: config.protocol+'://'+config.host+'/storage/'+userName,
        rel: "remoteStorage",
        type: "https://www.w3.org/community/rww/wiki/read-write-web-00#simple",
        properties: {
          'auth-method': "https://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2",
          'auth-endpoint': config.protocol+'://'+config.host+'/auth/'+userName
        }
      }]
    });
  }
  function oauth(urlObj, res) {
    console.log('OAUTH');
    var scopes = decodeURIComponent(urlObj.query['scope']).split(' '),
      clientId = decodeURIComponent(urlObj.query['client_id']),
      redirectUri = decodeURIComponent(urlObj.query['redirect_uri']),
      clientIdToMatch,
      userName;
    if(redirectUri.split('://').length<2) {
      clientIdToMatch=redirectUri;
    } else {
      clientIdToMatch = redirectUri.split('://')[1].split('/')[0];
    }
    if(clientId != clientIdToMatch) {
      writeHtml(res, 'we do not trust this combination of client_id and redirect_uri');
    } else {
      var userName = urlObj.pathname.substring('/auth/'.length);
      createToken(userName, scopes, function(token) {
        writeHtml(res, 'Allow');
      });
    }
  }
  function storage(req, urlObj, res) {
    var path=urlObj.pathname.substring('/storage/'.length);
    if(req.method=='OPTIONS') {
      console.log('OPTIONS ', req.headers);
      writeJson(res, null, req.headers.origin);
    } else if(req.method=='GET') {
      console.log('GET');
      if(mayRead(req.headers.authorization, path)) {
        if(content[path]) {
          if(path.substr(-1)=='/') {
            writeJson(res, content[path], req.headers.origin, 0);
          } else {
            writeRaw(res, contentType[path], content[path], req.headers.origin, lastModified[path]);
          }
        } else {
          if(path.substr(-1)=='/') {
            writeJson(res, {}, req.headers.origin, 0);
          } else {
            give404(res, req.headers.origin);
          }
        }
      } else {
        computerSaysNo(res, req.headers.origin);
      }
    } else if(req.method=='PUT') {
      console.log('PUT');
      if(mayWrite(req.headers.authorization, path) && path.substr(-1)!='/') {
        var dataStr = '';
        req.on('data', function(chunk) {
          dataStr+=chunk;
        });
        req.on('end', function(chunk) {
          var timestamp = new Date().getTime();
          content[path]=dataStr;
          contentType[path]=req.headers['content-type'];
          console.log('stored '+path, content[path], contentType[path]);
          lastModified[path]=timestamp;
          saveData();
          var pathParts=path.split('/');
          var timestamp=new Date().getTime();
          console.log(pathParts);
          var fileItself=true;
          while(pathParts.length > 2) {
            var thisPart = pathParts.pop();
            if(fileItself) {
              fileItself=false;
            } else {
              thisPart += '/';
            }
            if(!content[pathParts.join('/')+'/']) {
              content[pathParts.join('/')+'/'] = {};
            }
            content[pathParts.join('/')+'/'][thisPart]=timestamp;
            console.log('stored parent '+pathParts.join('/')+'/ ['+thisPart+']='+timestamp, content[pathParts.join('/')+'/']);
          }
          console.log('content:', content);
          console.log('contentType:', contentType);
          console.log('lastModified:', lastModified);
          writeJson(res, null, req.headers.origin, timestamp);
        });
      } else {
        computerSaysNo(res, req.headers.origin);
      }
    } else if(req.method=='DELETE') {
      console.log('DELETE');
      if(mayWrite(req.headers.authorization, path)) {
          var timestamp = new Date().getTime();
          delete content[path];
          delete contentType[path];
          lastModified[path]=timestamp;
          saveData();
          var pathParts=path.split('/');
          var thisPart = pathParts.pop();
          if(content[pathParts.join('/')+'/']) {
            console.log('delete content['+pathParts.join('/')+'/]['+thisPart+']');
            delete content[pathParts.join('/')+'/'][thisPart];
          }
          console.log(content);
          writeJson(res, null, req.headers.origin, timestamp);
      } else {
        computerSaysNo(res, req.headers.origin);
      }
    } else {
      console.log('ILLEGAL '+req.method);
      computerSaysNo(res, req.headers.origin);
    }
  }
  function serve(req, res, staticsMap) {
    var urlObj = url.parse(req.url, true), userAddress, userName;
    console.log(urlObj);
    if(urlObj.pathname == '/') {
      console.log('PORTAL');
      portal(urlObj, res);
    } else if(urlObj.pathname == '/.well-known/host-meta.json') {//TODO: implement rest of webfinger
      console.log('HOST-META');
      webfinger(urlObj, res);
    } else if(urlObj.pathname.substring(0, '/auth/'.length) == '/auth/') {
      console.log('OAUTH');
      oauth(urlObj, res);
    } else if(urlObj.pathname.substring(0, '/storage/'.length) == '/storage/') {
      console.log('STORAGE');
      storage(req, urlObj, res);
    } else {
      console.log('UNKNOWN');
      writeJson(res, urlObj.query);
    }
  }

  loadData();
  createInitialTokens();

  return {
    serve: serve
  };
})();

if(require.main==module) {//if this file is directly called from the CLI
  require('http').createServer(exports.handler.serve).listen(config.port);
  console.log("Example server started on 0.0.0.0:" + config.port);
}

<|endoftext|>