diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 61357bf13d..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,174 +0,0 @@ -version: 2.1 -orbs: - samvera: samvera/circleci-orb@1 -jobs: - build: - docker: - # Primary container image where all steps run. - - image: avalonmediasystem/avalon:develop - environment: - - DATABASE_URL=postgresql://postgres@localhost:5432/postgres - - FEDORA_URL=http://localhost:8080/fcrepo/rest - - FEDORA_TIMEOUT=300 - - RAILS_ENV=test - # Secondary container image on common network. - - image: postgres:10-alpine - environment: - - POSTGRES_USER=postgres - - POSTGRES_DB=avalon - - POSTGRES_PASSWORD=password - - image: ualbertalib/docker-fcrepo4:4.7 - environment: - CATALINA_OPTS: '-Djava.awt.headless=true -Dfile.encoding=UTF-8 -server -Xms512m -Xmx1024m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:PermSize=256m -XX:MaxPermSize=256m -XX:+DisableExplicitGC' - - image: zookeeper:3.9 - environment: - ZOO_ADMINSERVER_ENABLED: false - - image: solr:9 - environment: - VERBOSE: yes - SECURITY_JSON: '{"authentication":{"blockUnknown": false, "class":"solr.BasicAuthPlugin", "credentials":{"solr":"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c="}, "realm":"My Solr users", "forwardCredentials": false}, "authorization":{ "class":"solr.RuleBasedAuthorizationPlugin", "permissions":[{"name":"security-edit", "role":"admin"}], "user-role":{"solr":"admin"}}}' - command: sh -c "server/scripts/cloud-scripts/zkcli.sh -zkhost localhost:2181 -cmd put /security.json \"${SECURITY_JSON}\" && solr-fg -cloud -noprompt -p << parameters.solr_port >> -z localhost:2181" - - image: redis:alpine - - parameters: - ruby_ver: - description: 'Ruby version' - default: '3.1' - type: 'string' - solr_port: - type: string - default: '8985' - parallelism: - type: integer - default: 4 - solr_config_path: - type: string - default: 'solr/conf' - core_name: - type: string - default: 'hydra-test' - - working_directory: /home/app/avalon - - parallelism: << parameters.parallelism >> - - steps: - - run: - name: Clean out existing code - command: rm -rf .[!.]* * - - - run: git config --global --add safe.directory /home/app/avalon - - - samvera/cached_checkout - - - run: cp config/controlled_vocabulary.yml.example config/controlled_vocabulary.yml - - - run: - command: | - unset BUNDLE_APP_CONFIG - bundle install --with aws development test postgres --without production --jobs=4 --retry=3 - bundle exec rake db:migrate - - - restore_cache: - keys: - - yarn-cache-v1-{{ arch }}-{{ checksum "yarn.lock" }} - - - run: yarn - - - save_cache: - key: yarn-cache-v1-{{ arch }}-{{ checksum "yarn.lock" }} - paths: - - .cache/yarn - - # Copy solr 8 compliant step from unreleased version of orb - #- samvera/install_solr_core: - # solr_config_path: << parameters.solr_config_path >> - - run: - name: Wait for Solr - command: dockerize -wait tcp://localhost:<< parameters.solr_port >> -timeout 1m - - run: - name: Create solr core - command: | - if [ -d << parameters.solr_config_path >> ] - then - cd << parameters.solr_config_path >> - else - if [ -d "$(bundle show active-fedora)/lib/generators/active_fedora/config/solr/templates/solr/conf" ] - then - cd "$(bundle show active-fedora)/lib/generators/active_fedora/config/solr/templates/solr/conf" - else - cd "$(bundle show active-fedora)/lib/generators/active_fedora/config/solr/templates/solr/config" - fi - fi - zip -1 -r solr_conf.zip ./* - curl -H "Content-type:application/octet-stream" --data-binary @solr_conf.zip "http://solr:SolrRocks@127.0.0.1:<< parameters.solr_port >>/solr/admin/configs?action=UPLOAD&name=solrconfig" - curl "http://solr:SolrRocks@127.0.0.1:<< parameters.solr_port >>/solr/admin/collections?action=CREATE&name=hydra-test&numShards=1&collection.configName=solrconfig" - - - run: - command: | - if [[ $(command -v cc-test-reporter) == "" ]]; then - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - fi - - run: - command: ./cc-test-reporter before-build - - # Pull in the parallel_rspec step and modify it to ensure that test results get stored - # - samvera/parallel_rspec - - run: mkdir /tmp/test-results - - run: - name: Run rspec in parallel - command: | - circleci tests glob "spec/**/*_spec.rb" | circleci tests run --command="xargs bundle exec rspec --format progress --format RspecJunitFormatter -o /tmp/test-results/rspec.xml" --verbose --split-by=timings - # collect reports - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: /tmp/test-results - destination: test-results - - - run: - command: ./cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json" - - - persist_to_workspace: - root: coverage - paths: - - codeclimate.*.json - - upload-coverage: - parameters: - parallelism: - type: integer - default: 4 - docker: - # Primary container image where all steps run. - - image: avalonmediasystem/avalon:7.5.0-dev-ruby3 - - working_directory: /home/app/avalon - - steps: - - attach_workspace: - at: /home/app/avalon - - - run: - name: Install Code Climate Test Reporter - command: | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - - - deploy: - # CC_TEST_REPORTER_ID set within the circleci web interface - command: ./cc-test-reporter sum-coverage --output - --parts << parameters.parallelism >> codeclimate.*.json | ./cc-test-reporter upload-coverage --debug --input - - -workflows: - version: 2 - build_test_report: - jobs: - - build: - ruby_ver: '3.2' - name: 'Ruby3-2' - parallelism: 4 - - upload-coverage: - parallelism: 4 - requires: - - Ruby3-2 diff --git a/.dockerignore b/.dockerignore index 662e28b6b9..db4e86f450 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,10 @@ .env Dockerfile docker-compose.yml +encodes +log node_modules -public/packs +public/packs* tmp +vendor/bundle +terraform diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..ee30b20202 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,26 @@ +### Severity + +Is the production site running? +- [ ] yes +- [ ] no + +Are staff blocked from performing their work? +- [ ] yes +- [ ] no + +### Descriptive summary + + Include any relevant tracebacks if you're reporting a bug. + +### Expected behavior + +### Actual behavior + +### Steps to reproduce the behavior + +1. Do this +1. Then do this... + +### Related work + +Link to related tickets or prior related work here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..7dacd8180f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ +Fixes #issuenumber ; refs #issuenumber + +Present tense short summary (50 characters or less) + +More detailed description, if necessary. Try to be as descriptive as you can: even if you think that the PR content is obvious, it may not be obvious to others. Include tracebacks if helpful, and be sure to call out any bits of the PR that may be work-in-progress. + +Description can have multiple paragraphs and you can use code examples inside: + +``` ruby +class PostsController + def index + respond_with Post.limit(10) + end +end +``` + +Changes proposed in this pull request: +* +* +* + +## Documentation +Features should have extensive wiki documentation for developers and end users if necessary. Puppet changes or small config changes can self document. Link to wiki doc or code lines in repo that documents changes: + +* +* diff --git a/.github/scripts/configure_aws.sh b/.github/scripts/configure_aws.sh new file mode 100755 index 0000000000..0d8c0b0453 --- /dev/null +++ b/.github/scripts/configure_aws.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +environment=$(echo $DEPLOY_ENV | tr a-z A-Z) +access_key_id_var=${environment}_AWS_ACCESS_KEY_ID +secret_key_var=${environment}_AWS_SECRET_ACCESS_KEY +echo "Configuring AWS default profile with ${access_key_id_var} and ${secret_key_var}" + +aws configure set aws_access_key_id $(jq -r ".${access_key_id_var}" <<< $SECRETS) +aws configure set aws_secret_access_key $(jq -r ".${secret_key_var}" <<< $SECRETS) +aws configure set default.region us-east-1 diff --git a/.github/scripts/honeybadger_deploy_notification.sh b/.github/scripts/honeybadger_deploy_notification.sh new file mode 100755 index 0000000000..34d93ce33d --- /dev/null +++ b/.github/scripts/honeybadger_deploy_notification.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +curl \ + --data "deploy[environment]=${DEPLOY_ENV}&deploy[local_username]=Github+Actions&deploy[revision]=${HONEYBADGER_REVISION}&api_key=${HONEYBADGER_API_KEY}" \ + https://api.honeybadger.io/v1/deploys diff --git a/.github/scripts/update_ecs_service.sh b/.github/scripts/update_ecs_service.sh new file mode 100755 index 0000000000..b8080623c8 --- /dev/null +++ b/.github/scripts/update_ecs_service.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +networkconfig=$(aws ecs describe-services --cluster ${ECS_CLUSTER} --service ${ECS_SERVICE} | jq -cM '.services[0].networkConfiguration') +overrides='{"containerOverrides":[{"name":"'${ECS_CONTAINER}'","environment": [{"name": "CONTAINER_ROLE", "value": "migrate"}]}]}' +aws ecs run-task --platform-version 1.4.0 --cluster ${ECS_CLUSTER} --task-definition ${ECS_TASK} --overrides "${overrides}" --launch-type FARGATE --network-configuration ${networkconfig} +for service in $(aws ecs list-services --cluster ${ECS_CLUSTER} | jq -r '.serviceArns[] | split("/") | last'); do + aws ecs update-service --cluster ${ECS_CLUSTER} --service ${service} --force-new-deployment +done diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..92402f3ca8 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,143 @@ +--- +name: Build and Deploy +on: [push] +jobs: + test: + if: ${{ ! (startsWith(github.ref, 'refs/heads/nu/deploy/') || startsWith(github.ref, 'refs/heads/nu/build/')) }} + runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: minio + AWS_SECRET_ACCESS_KEY: minio123 + BUNDLE_WITH: aws:postgres + BUNDLE_WITHOUT: production + CFLAGS: -Wno-error=format-overflow + RACK_ENV: test + RAILS_ENV: test + services: + db: + image: nulib/postgres:10-alpine + env: + POSTGRES_USER: docker + POSTGRES_PASSWORD: d0ck3r + ports: + - 5432:5432 + fcrepo: + image: samvera/fcrepo4:4.7.5 + env: + JAVA_OPTIONS: -Xmx3G + ports: + - 8986:8080 + minio: + image: bitnami/minio + env: + MINIO_ROOT_USER: minio + MINIO_ROOT_PASSWORD: minio123 + MINIO_ACCESS_KEY: minio + MINIO_SECRET_KEY: minio123 + ports: + - 9002:9000 + redis: + image: circleci/redis:alpine + ports: + - 6379:6379 + solr: + image: nulib/solrcloud:7.2-alpine + env: + JAVA_OPTIONS: -Xmx3G + ports: + - 8985:8983 + - 9985:9983 + steps: + - name: Install Dependencies + run: | + sudo apt-get update -qq --allow-releaseinfo-change + sudo apt-get install gcc-9 lsof mediainfo + sudo ln -s /usr/bin/lsof /usr/sbin/lsof + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6 + bundler-cache: true + - uses: actions/setup-node@v2 + with: + node-version: '14' + cache: 'yarn' + - name: Install JS dependencies + run: yarn install + - name: Setup + run: | + echo "Uploading solr collection config to $(bundle exec rails runner 'puts Settings.zookeeper.connection_str')" + bundle exec rake db:setup zookeeper:upload zookeeper:create + cp config/controlled_vocabulary.yml.example config/controlled_vocabulary.yml + - name: Run Spec Tests + run: bundle exec rspec -cf doc spec + publish: + if: ${{ (!github.event.pull_request) && (startsWith(github.ref, 'refs/heads/nu/deploy/') || startsWith(github.ref, 'refs/heads/nu/build/')) }} + runs-on: ubuntu-latest + env: + AWS_SDK_LOAD_CONFIG: 1 + steps: + - uses: actions/checkout@v2 + - name: Set DEPLOY_ENV from Branch Name + run: | + echo "DEPLOY_ENV=$(echo $BRANCH | awk -F/ '{print $NF}')" >> $GITHUB_ENV + env: + BRANCH: ${{ github.ref }} + - name: Configure AWS + run: .github/scripts/configure_aws.sh + env: + DEPLOY_ENV: ${{ env.DEPLOY_ENV }} + SECRETS: ${{ toJSON(secrets) }} + - run: echo "Building nulib/avr:${DEPLOY_ENV}" + - uses: docker/setup-buildx-action@v1 + with: + install: true + - uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_LOGIN }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + - name: Build, tag, and push image to Amazon ECR + run: | + docker build --push -t $ECR_REGISTRY/$ECR_REPOSITORY:$DEPLOY_ENV --target=prod . + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: avr + deploy: + needs: publish + if: ${{ ! startsWith(github.ref, 'refs/heads/nu/build/') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 2 + - name: Set DEPLOY_ENV from Branch Name + run: | + if [[ $BRANCH == 'refs/heads/master' ]]; then + echo "DEPLOY_ENV=production" >> $GITHUB_ENV + else + echo "DEPLOY_ENV=$(echo $BRANCH | awk -F/ '{print $NF}')" >> $GITHUB_ENV + fi + env: + BRANCH: ${{ github.ref }} + - name: Configure AWS + run: .github/scripts/configure_aws.sh + env: + DEPLOY_ENV: ${{ env.DEPLOY_ENV }} + SECRETS: ${{ toJSON(secrets) }} + - name: Update ECS Service + run: .github/scripts/update_ecs_service.sh + env: + ECS_CLUSTER: avr + ECS_CONTAINER: avr + ECS_SERVICE: avr-worker + ECS_TASK: avr-worker + - name: Notify Honeybadger + run: .github/scripts/honeybadger_deploy_notification.sh + env: + DEPLOY_ENV: ${{ env.DEPLOY_ENV }} + HONEYBADGER_API_KEY: ${{ secrets.HONEYBADGER_API_KEY }} + HONEYBADGER_REVISION: ${{ github.sha }} + \ No newline at end of file diff --git a/.github/workflows/tflint.yml b/.github/workflows/tflint.yml new file mode 100644 index 0000000000..1a3e08bcd6 --- /dev/null +++ b/.github/workflows/tflint.yml @@ -0,0 +1,29 @@ +--- +name: Check Terraform Style +on: [push] +jobs: + tflint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + name: Checkout source code + + - uses: actions/cache@v2 + name: Cache plugin dir + with: + path: ~/.tflint.d/plugins + key: tflint-${{ hashFiles('.tflint.hcl') }} + + - uses: terraform-linters/setup-tflint@v1 + name: Setup tflint + with: + tflint_version: v0.52.0 + - name: Show version + run: tflint --version + + - name: Check Terraform Manifests + run: | + terraform init -backend=false -input=false + tflint --init + tflint -f compact + working-directory: ./terraform diff --git a/.gitignore b/.gitignore index 96e25700a6..7b8f14a549 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ config/initializers/rubyhorn.rb config/secrets.yml #config/lti.yml +public/assets public/media_objects public/streams public/**/*.m3u8 @@ -90,3 +91,12 @@ yarn-debug.log* .*.swo .*.swp +# ActiveEncode +/encodes + +terraform/**/node_modules +terraform/package +terraform/.terraform +terraform/**/*.tfvars +terraform/*.plan +config/shoryuken.yml diff --git a/Dockerfile b/Dockerfile index 0942a04acc..4bbef4f862 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Base stage for building gems -FROM ruby:3.2-bullseye as bundle +FROM ruby:3.2-bullseye AS bundle LABEL stage=build LABEL project=avalon RUN apt-get update && apt-get upgrade -y build-essential && apt-get autoremove \ @@ -13,27 +13,32 @@ RUN apt-get update && apt-get upgrade -y build-essential && apt-get autor && rm -rf /var/lib/apt/lists/* \ && apt-get clean -COPY Gemfile ./Gemfile -COPY Gemfile.lock ./Gemfile.lock +ENV BUILD_DEPS="build-essential libpq-dev libsqlite3-dev libwrap0-dev tzdata locales git curl unzip shared-mime-info" \ + DEBIAN_FRONTEND="noninteractive" \ + RAILS_ENV="production" \ + LANG="en_US.UTF-8" -RUN gem install bundler -v "$(grep -A 1 "BUNDLED WITH" Gemfile.lock | tail -n 1)" \ - && bundle config build.nokogiri --use-system-libraries +RUN useradd -m -U app \ + && su -s /bin/bash -c "mkdir -p /home/app" app +RUN apt-get update -qq && apt-get install -y $BUILD_DEPS -ENV RUBY_THREAD_MACHINE_STACK_SIZE 8388608 -ENV RUBY_THREAD_VM_STACK_SIZE 8388608 +ENV RUBY_THREAD_MACHINE_STACK_SIZE=8388608 +ENV RUBY_THREAD_VM_STACK_SIZE=8388608 # Build development gems -FROM bundle as bundle-dev +FROM bundle AS bundle-dev LABEL stage=build LABEL project=avalon -RUN bundle config set --local without 'production' \ +RUN bundle config set --local without 'production zoom' \ && bundle config set --local with 'aws development test postgres' \ && bundle install +RUN gem update --system \ + && chown -R app:staff /usr/local/bundle # Download binaries in parallel -FROM ruby:3.2-bullseye as download +FROM ruby:3.2-bullseye AS download LABEL stage=build LABEL project=avalon RUN curl -L https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-linux-amd64-v0.6.1.tar.gz | tar xvz -C /usr/bin/ @@ -44,9 +49,14 @@ RUN curl https://chromedriver.storage.googleapis.com/index.html?path=${c && chmod +x /usr/local/bin/chromedriver RUN apt-get -y update && apt-get install -y ffmpeg +COPY --chown=app:app Gemfile* /home/app/ +ENV BUNDLE_WITH='aws:postgres' BUNDLE_WITHOUT='development:test:zoom' +RUN bundle install --jobs $(nproc) --retry 5 +RUN find /usr/local/bundle/ -name '*.gem' -or -name '*.c' -or -name '*.o' -delete +RUN rm -rf /usr/local/bundle/**/.git # Base stage for building final images -FROM ruby:3.2-slim-bullseye as base +FROM ruby:3.2-slim-bullseye AS base LABEL stage=build LABEL project=avalon RUN echo "deb http://ftp.us.debian.org/debian/ bullseye main contrib non-free" > /etc/apt/sources.list.d/bullseye.list \ @@ -77,6 +87,7 @@ RUN apt-get update && \ zip \ dumb-init \ libsqlite3-dev \ + sudo \ && apt-get -y install mediainfo \ && ln -s /usr/bin/lsof /usr/sbin/ @@ -86,34 +97,35 @@ WORKDIR /home/app/avalon # Build devevelopment image -FROM base as dev +FROM base AS dev LABEL stage=final LABEL project=avalon RUN apt-get update && apt-get install -y --no-install-recommends --allow-unauthenticated \ build-essential \ cmake -COPY --from=bundle-dev /usr/local/bundle /usr/local/bundle -COPY --from=download /chrome.deb / -COPY --from=download /usr/local/bin/chromedriver /usr/local/bin/chromedriver -COPY --from=download /usr/bin/dockerize /usr/bin/ -ADD docker_init.sh / +COPY --chown=app:staff --from=ruby-deps /usr/local/bundle /usr/local/bundle +COPY --chown=app:app --from=npm-deps /home/app/node_modules/ /home/app/node_modules/ +COPY --chown=app:app . /home/app/ -ARG RAILS_ENV=development -RUN dpkg -i /chrome.deb || apt-get install -yf +RUN mkdir /var/run/puma && chown root:app /var/run/puma && chmod 0775 /var/run/puma +USER app +WORKDIR /home/app +ENV BUNDLE_WITH='aws:postgres' BUNDLE_WITHOUT='development:test:zoom' +RUN bundle exec rake assets:precompile SECRET_KEY_BASE=$(ruby -r 'securerandom' -e 'puts SecureRandom.hex(64)') # Build production gems -FROM bundle as bundle-prod +FROM bundle AS bundle-prod LABEL stage=build LABEL project=avalon -RUN bundle config set --local without 'development test' \ +COPY Gemfile* . +RUN bundle config set --local without 'development test zoom' \ && bundle config set --local with 'aws production postgres' \ && bundle install - # Install node modules -FROM node:20-bullseye-slim as node-modules +FROM node:20-bullseye-slim AS node-modules LABEL stage=build LABEL project=avalon RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates @@ -123,7 +135,7 @@ RUN yarn install # Build production assets -FROM base as assets +FROM base AS assets LABEL stage=build LABEL project=avalon COPY --from=bundle-prod --chown=app:app /usr/local/bundle /usr/local/bundle @@ -138,11 +150,16 @@ RUN cp config/controlled_vocabulary.yml.example config/controlled_vocabu # Build production image -FROM base as prod +FROM base AS prod LABEL stage=final LABEL project=avalon COPY --from=assets --chown=app:app /home/app/avalon /home/app/avalon COPY --from=bundle-prod --chown=app:app /usr/local/bundle /usr/local/bundle +RUN mkdir /var/run/puma && chown root:app /var/run/puma && chmod 0775 /var/run/puma USER app ENV RAILS_ENV=production +ENV PATH="/home/app/bin:${PATH}" +EXPOSE 3000 +CMD bin/boot_container +HEALTHCHECK --start-period=60s CMD curl -f http://localhost:3000/ \ No newline at end of file diff --git a/Dockerfile.runtime b/Dockerfile.runtime new file mode 100644 index 0000000000..5491e27f47 --- /dev/null +++ b/Dockerfile.runtime @@ -0,0 +1,40 @@ +#################################### +# Shared runtime image +FROM ruby:2.6.6-slim-stretch as runtime + +RUN useradd -m -U app \ + && su -s /bin/bash -c "mkdir -p /home/app/vendor/gems" app + +ENV RUNTIME_DEPS="git imagemagick libexif12 libexpat1 libgif7 glib-2.0 libgsf-1-114 libjpeg62-turbo libpng16-16 libpoppler-glib8 libpq5 libreoffice-core librsvg2-2 libsqlite3-0 libtiff5 libwrap0 libyaz4 locales mediainfo nodejs openjdk-8-jre-headless shared-mime-info sudo tzdata yarn" \ + DEBIAN_FRONTEND="noninteractive" \ + RAILS_ENV="production" \ + LANG="en_US.UTF-8" + +RUN mkdir /usr/share/man/man1 \ + && apt-get update -qq \ + && apt-get install -y curl gnupg2 apt-transport-https ca-certificates --no-install-recommends \ + && curl -sL https://deb.nodesource.com/setup_12.x | bash - \ + && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb http://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ + && curl -LO https://mediaarea.net/repo/deb/repo-mediaarea_1.0-16_all.deb \ + && dpkg -i repo-mediaarea_1.0-16_all.deb \ + && rm repo-mediaarea_1.0-16_all.deb \ + && apt-get update -qq \ + && apt-get install -y $RUNTIME_DEPS --no-install-recommends \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* \ + && alias nodejs=node \ + && yarn add webpack \ + && dpkg-reconfigure -f noninteractive tzdata \ + && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ + && echo 'LANG="en_US.UTF-8"' > /etc/default/locale \ + && dpkg-reconfigure --frontend=noninteractive locales \ + && update-locale LANG=en_US.UTF-8 + +# Install FFMPEG +RUN mkdir -p /tmp/ffmpeg/bin \ + && cd /tmp/ffmpeg \ + && curl https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz | tar xJ \ + && cp $(find . -type f -executable) /usr/local/bin/ + +RUN gem update --system diff --git a/Gemfile b/Gemfile index e108dde2c8..527c152ff9 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ gem 'psych', '< 4' gem 'rails', '~>7.0.8' gem 'sprockets', '~>3.7.2' #gem 'sprockets-rails', require: 'sprockets/railtie' -gem 'sqlite3' +gem 'sqlite3', '~>1.4' # Force newer version of mail for compatibility with rails 6.0.6.1 gem 'mail', '> 2.8.0.1' @@ -152,7 +152,6 @@ group :test do end group :production do - gem 'google-analytics-rails', '1.1.0' gem 'lograge' gem 'okcomputer' gem 'puma', '>= 6.4.2' diff --git a/Gemfile.local b/Gemfile.local new file mode 100644 index 0000000000..57a90c555e --- /dev/null +++ b/Gemfile.local @@ -0,0 +1,17 @@ +gem 'aws-sdk-cloudwatchevents' +gem 'aws-sdk-cloudwatchlogs' +gem 'aws-sdk-lambda' +gem 'aws-sdk-mediaconvert' +gem 'aws-sdk-ssm' +gem 'ezid-client' +gem 'honeybadger', '~> 4.0' +gem 'omniauth-nusso', git: 'https://github.com/nulib/omniauth-nusso', tag: 'v0.2.0' + +group :aws do + gem 'shoryuken' +end + +group :development do + gem 'guard-puma' + gem 'guard-process' +end diff --git a/Gemfile.lock b/Gemfile.lock index a1027bec21..2524f98014 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,15 @@ GIT ims-lti omniauth +GIT + remote: https://github.com/nulib/omniauth-nusso + revision: 6cedbcadd29ab529782c15adab7bc9cdfc2ddbf4 + tag: v0.2.0 + specs: + omniauth-nusso (0.2.0) + faraday + omniauth + GEM remote: https://rubygems.org/ specs: @@ -132,9 +141,9 @@ GEM json-ld rails (>= 5.2, < 7.1) rdf-vocab (>= 2.1.0) - active_elastic_job (3.2.0) + active_elastic_job (3.3.0) aws-sdk-sqs (~> 1) - rails (>= 5.2.6, < 7.1) + rails (>= 5.2.6, < 8) active_encode (1.2.2) addressable (~> 2.8) rails @@ -152,20 +161,21 @@ GEM activejob (>= 4.2) activesupport (>= 4.2) suo - activejob-uniqueness (0.2.5) - activejob (>= 4.2, < 7.1) - redlock (>= 1.2, < 2) + activejob-uniqueness (0.3.2) + activejob (>= 4.2, < 7.3) + redlock (>= 2.0, < 3) activemodel (7.0.8.4) activesupport (= 7.0.8.4) activerecord (7.0.8.4) activemodel (= 7.0.8.4) activesupport (= 7.0.8.4) - activerecord-session_store (2.0.0) - actionpack (>= 5.2.4.1) - activerecord (>= 5.2.4.1) + activerecord-session_store (2.1.0) + actionpack (>= 6.1) + activerecord (>= 6.1) + cgi (>= 0.3.6) multi_json (~> 1.11, >= 1.11.2) - rack (>= 2.0.8, < 3) - railties (>= 5.2.4.1) + rack (>= 2.0.8, < 4) + railties (>= 6.1) activestorage (7.0.8.4) actionpack (= 7.0.8.4) activejob (= 7.0.8.4) @@ -178,71 +188,96 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - acts_as_list (1.1.0) - activerecord (>= 4.2) - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) - airbrussh (1.4.1) + acts_as_list (1.2.2) + activerecord (>= 6.1) + activesupport (>= 6.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + airbrussh (1.5.3) sshkit (>= 1.6.1, != 1.7.0) - api-pagination (5.0.0) + api-pagination (6.0.0) ast (2.4.2) audio_waveform-ruby (1.0.7) json (~> 2.3) - autoprefixer-rails (10.4.7.0) + autoprefixer-rails (10.4.19.0) execjs (~> 2) - aws-eventstream (1.2.0) - aws-partitions (1.801.0) - aws-record (2.10.1) - aws-sdk-dynamodb (~> 1.18) - aws-sdk-cloudfront (1.76.0) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sigv4 (~> 1.1) - aws-sdk-core (3.171.0) - aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.651.0) + aws-eventstream (1.3.0) + aws-partitions (1.976.0) + aws-record (2.13.2) + aws-sdk-dynamodb (~> 1, >= 1.85.0) + aws-sdk-cloudfront (1.99.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sdk-cloudwatchevents (1.80.0) + aws-sdk-core (~> 3, >= 3.205.0) aws-sigv4 (~> 1.5) + aws-sdk-cloudwatchlogs (1.93.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sdk-core (3.205.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-dynamodb (1.81.0) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sigv4 (~> 1.1) - aws-sdk-elastictranscoder (1.40.0) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sigv4 (~> 1.1) - aws-sdk-kms (1.63.0) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sigv4 (~> 1.1) - aws-sdk-rails (3.7.1) + aws-sdk-dynamodb (1.122.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sdk-elastictranscoder (1.60.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sdk-kms (1.91.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sdk-lambda (1.130.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sdk-mediaconvert (1.138.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sdk-rails (4.0.3) + actionmailbox (>= 7.0.0) aws-record (~> 2) - aws-sdk-ses (~> 1) - aws-sdk-sesv2 (~> 1) - aws-sdk-sqs (~> 1) + aws-sdk-s3 (~> 1, >= 1.123.0) + aws-sdk-ses (~> 1, >= 1.50.0) + aws-sdk-sesv2 (~> 1, >= 1.34.0) + aws-sdk-sns (~> 1, >= 1.61.0) + aws-sdk-sqs (~> 1, >= 1.56.0) aws-sessionstore-dynamodb (~> 2) - concurrent-ruby (~> 1) - railties (>= 5.2.0) - aws-sdk-s3 (1.122.0) - aws-sdk-core (~> 3, >= 3.165.0) + concurrent-ruby (~> 1.3, >= 1.3.1) + railties (>= 7.0.0) + aws-sdk-s3 (1.162.0) + aws-sdk-core (~> 3, >= 3.205.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sdk-ses (1.49.0) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sigv4 (~> 1.1) - aws-sdk-sesv2 (1.31.0) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sigv4 (~> 1.1) - aws-sdk-sqs (1.55.0) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sigv4 (~> 1.1) - aws-sessionstore-dynamodb (2.0.1) - aws-sdk-dynamodb (~> 1) - rack (~> 2) - aws-sigv4 (1.6.0) + aws-sigv4 (~> 1.5) + aws-sdk-ses (1.72.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sdk-sesv2 (1.59.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sdk-sns (1.85.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sdk-sqs (1.83.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sdk-ssm (1.178.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sessionstore-dynamodb (2.2.0) + aws-sdk-dynamodb (~> 1, >= 1.85.0) + rack (>= 2, < 4) + rack-session (>= 1, < 3) + aws-sigv4 (1.9.1) aws-eventstream (~> 1, >= 1.0.2) babel-source (5.8.35) babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) execjs (~> 2.0) - bcrypt (3.1.18) - bigdecimal (3.1.6) + base64 (0.2.0) + bcp47_spec (0.2.1) + bcrypt (3.1.20) + bigdecimal (3.1.8) bindex (0.8.1) bixby (5.0.2) rubocop (= 1.28.2) @@ -250,7 +285,7 @@ GEM rubocop-performance rubocop-rails rubocop-rspec - blacklight (7.33.1) + blacklight (7.38.0) deprecation globalid hashdiff @@ -258,31 +293,30 @@ GEM jbuilder (~> 2.7) kaminari (>= 0.15) ostruct (>= 0.3.2) - rails (>= 5.1, < 7.1) - view_component (~> 2.66) + rails (>= 5.1, < 7.3) + view_component (>= 2.66, < 4) blacklight-access_controls (6.0.1) blacklight (> 6.0, < 8) cancancan (>= 1.8) deprecation (~> 1.0) - bootsnap (1.16.0) + bootsnap (1.18.4) msgpack (~> 1.2) - bootstrap (4.6.2) + bootstrap (4.6.2.1) autoprefixer-rails (>= 9.1.0) popper_js (>= 1.16.1, < 2) - sassc-rails (>= 2.0.0) bootstrap-toggle-rails (2.2.1.0) - bootstrap_form (5.2.3) - actionpack (>= 6.0) - activemodel (>= 6.0) + bootstrap_form (5.4.0) + actionpack (>= 6.1) + activemodel (>= 6.1) builder (3.3.0) byebug (11.1.3) - cancancan (3.4.0) - capistrano (3.17.3) + cancancan (3.6.1) + capistrano (3.19.1) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (2.1.0) + capistrano-bundler (2.1.1) capistrano (~> 3.1) capistrano-passenger (0.2.1) capistrano (~> 3.0) @@ -298,15 +332,16 @@ GEM sidekiq (>= 6.0) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.39.2) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + cgi (0.4.1) childprocess (3.0.0) cloudfront-signer (3.0.2) codeclimate-test-reporter (1.0.7) @@ -319,19 +354,19 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.3.3) - config (4.2.1) + concurrent-ruby (1.3.4) + config (5.5.1) deep_merge (~> 1.2, >= 1.2.1) - dry-validation (~> 1.0, >= 1.0.0) - connection_pool (2.3.0) - crack (0.4.5) + connection_pool (2.4.1) + crack (1.0.0) + bigdecimal rexml crass (1.0.6) daemons (1.4.1) - dalli (3.2.3) + dalli (3.2.8) database_cleaner (2.0.2) database_cleaner-active_record (>= 2, < 3) - database_cleaner-active_record (2.0.1) + database_cleaner-active_record (2.2.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) @@ -340,7 +375,7 @@ GEM deep_merge (1.2.2) deprecation (1.1.0) activesupport - devise (4.9.2) + devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -349,135 +384,132 @@ GEM devise_invitable (2.0.9) actionmailer (>= 5.0) devise (>= 4.6) - diff-lcs (1.5.0) - docile (1.4.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.8.1) - dotenv-rails (2.8.1) - dotenv (= 2.8.1) - railties (>= 3.2) + diff-lcs (1.5.1) + docile (1.4.1) + domain_name (0.6.20240107) + dotenv (3.1.2) + dotenv-rails (3.1.2) + dotenv (= 3.1.2) + railties (>= 6.1) dropbox_api (0.1.21) faraday (< 3.0) oauth2 (~> 1.1) - dry-configurable (1.0.1) - dry-core (~> 1.0, < 2) - zeitwerk (~> 2.6) - dry-core (1.0.0) - concurrent-ruby (~> 1.0) - zeitwerk (~> 2.6) - dry-inflector (1.0.0) - dry-initializer (3.1.1) - dry-logic (1.5.0) - concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) - zeitwerk (~> 2.6) - dry-schema (1.13.0) - concurrent-ruby (~> 1.0) - dry-configurable (~> 1.0, >= 1.0.1) - dry-core (~> 1.0, < 2) - dry-initializer (~> 3.0) - dry-logic (>= 1.5, < 2) - dry-types (>= 1.7, < 2) - zeitwerk (~> 2.6) - dry-types (1.7.0) - concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) - dry-inflector (~> 1.0, < 2) - dry-logic (>= 1.4, < 2) - zeitwerk (~> 2.6) - dry-validation (1.10.0) - concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) - dry-initializer (~> 3.0) - dry-schema (>= 1.12, < 2) - zeitwerk (~> 2.6) - ebnf (2.3.2) + ebnf (2.4.0) htmlentities (~> 4.3) - rdf (~> 3.2) + rdf (~> 3.3) scanf (~> 1.0) - sxp (~> 1.2) + sxp (~> 1.3) unicode-types (~> 1.8) edtf (3.1.1) activesupport (>= 3.0, < 8.0) - email_spec (2.2.2) + email_spec (2.3.0) htmlentities (~> 4.3.3) - launchy (~> 2.1) + launchy (>= 2.1, < 4.0) mail (~> 2.7) equivalent-xml (0.6.0) nokogiri (>= 1.4.3) erubi (1.13.0) - et-orbi (1.2.7) + et-orbi (1.2.11) tzinfo ethon (0.16.0) ffi (>= 1.15.0) - execjs (2.8.1) - factory_bot (6.2.1) + execjs (2.9.1) + ezid-client (1.11.0) + hashie (~> 3.4, >= 3.4.3) + nokogiri + factory_bot (6.5.0) activesupport (>= 5.0.0) - factory_bot_rails (6.2.0) - factory_bot (~> 6.2.0) + factory_bot_rails (6.4.3) + factory_bot (~> 6.4) railties (>= 5.0.0) fakefs (2.5.0) - faker (3.2.0) + faker (3.4.2) i18n (>= 1.8.11, < 2) - faraday (2.7.4) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-encoding (0.0.5) + faraday (2.11.0) + faraday-net_http (>= 2.0, < 3.4) + logger + faraday-encoding (0.0.6) faraday - faraday-net_http (3.0.2) - fastimage (2.2.7) + faraday-net_http (3.3.0) + net-http + fastimage (2.3.1) fcrepo_wrapper (0.9.0) ruby-progressbar - ffi (1.15.5) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) + ffi (1.17.0-x86_64-linux-gnu) + ffi-compiler (1.3.2) + ffi (>= 1.15.5) rake font-awesome-rails (4.7.0.8) railties (>= 3.2, < 8.0) - fugit (1.8.1) - et-orbi (~> 1, >= 1.2.7) + formatador (1.1.0) + fugit (1.11.1) + et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - google-analytics-rails (1.1.0) - google-apis-core (0.11.0) + google-apis-core (0.15.1) addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) mini_mime (~> 1.0) + mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - rexml - webrick - google-apis-drive_v3 (0.37.0) - google-apis-core (>= 0.11.0, < 2.a) - googleauth (1.5.0) - faraday (>= 0.17.3, < 3.a) + google-apis-drive_v3 (0.56.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-env (2.2.0) + faraday (>= 1.0, < 3.a) + googleauth (1.11.0) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - hashdiff (1.0.1) - hashie (5.0.0) + guard (2.18.1) + formatador (>= 0.2.4) + listen (>= 2.7, < 4.0) + lumberjack (>= 1.0.12, < 2.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.13.0) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-compat (1.2.1) + guard-process (1.2.1) + guard-compat (~> 1.2, >= 1.2.1) + spoon (~> 0.0.1) + guard-puma (0.8.1) + guard (~> 2.14) + guard-compat (~> 1.2) + puma (>= 4.0, < 7) + haml (6.3.0) + temple (>= 0.8.2) + thor + tilt + hamster (3.0.0) + concurrent-ruby (~> 1.0) + hashdiff (1.1.1) + hashie (3.6.0) + honeybadger (4.12.2) hooks (0.4.1) uber (~> 0.0.14) htmlentities (4.3.4) - http-2-next (1.0.3) - http (5.1.1) + http-2 (1.0.1) + http (5.2.0) addressable (~> 2.8) + base64 (~> 0.1) http-cookie (~> 1.0) http-form_data (~> 2.2) - llhttp-ffi (~> 0.4.0) + llhttp-ffi (~> 0.5.0) http-accept (1.7.0) - http-cookie (1.0.5) + http-cookie (1.0.7) domain_name (~> 0.5) http-form_data (2.3.0) http_logger (0.7.0) httpclient (2.8.3) - httpx (1.2.2) - http-2-next (>= 1.0.3) + httpx (1.3.1) + http-2 (>= 1.0.0) hydra-access-controls (12.1.0) active-fedora (>= 10.0.0) activesupport (>= 5.2, < 7.1) @@ -491,7 +523,7 @@ GEM hydra-access-controls (= 12.1.0) hydra-core (= 12.1.0) rails (>= 5.2, < 7.1) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) iconv (1.0.8) iiif_manifest (1.6.0) @@ -499,7 +531,7 @@ GEM ims-lti (1.1.13) builder oauth (>= 0.4.5, < 0.6) - jbuilder (2.11.5) + jbuilder (2.13.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) @@ -510,16 +542,21 @@ GEM thor (>= 0.14, < 2.0) jquery-ui-rails (7.0.0) railties (>= 3.2.16) - json (2.6.3) - json-canonicalization (0.3.1) - json-ld (3.2.3) + json (2.7.2) + json-canonicalization (1.0.0) + json-ld (3.3.2) htmlentities (~> 4.3) - json-canonicalization (~> 0.3) + json-canonicalization (~> 1.0) link_header (~> 0.0, >= 0.0.8) multi_json (~> 1.15) - rack (~> 2.2) - rdf (~> 3.2, >= 3.2.9) - jwt (2.7.0) + rack (>= 2.2, < 4) + rdf (~> 3.3) + rexml (~> 3.2) + json-ld-preloaded (3.3.0) + json-ld (~> 3.3) + rdf (~> 3.3) + jwt (2.9.0) + base64 kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -534,6 +571,12 @@ GEM kaminari-core (1.2.2) launchy (2.5.2) addressable (~> 2.8) + ld-patch (3.3.0) + ebnf (~> 2.4) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + sparql (~> 3.3) + sxp (~> 1.3) ldp (1.1.0) deprecation faraday (>= 1) @@ -546,13 +589,42 @@ GEM rdf-vocab (>= 0.8) slop link_header (0.0.8) - listen (3.8.0) + linkeddata (3.3.1) + json-ld (~> 3.3) + json-ld-preloaded (~> 3.3) + ld-patch (~> 3.3) + nokogiri (~> 1.15, >= 1.15.4) + rdf (~> 3.2, >= 3.2.1) + rdf-aggregate-repo (~> 3.2) + rdf-hamster-repo (~> 3.3) + rdf-isomorphic (~> 3.3) + rdf-json (~> 3.3) + rdf-microdata (~> 3.3) + rdf-n3 (~> 3.3) + rdf-normalize (~> 0.7) + rdf-ordered-repo (~> 3.3) + rdf-rdfa (~> 3.3) + rdf-rdfxml (~> 3.3) + rdf-reasoner (~> 0.9) + rdf-tabular (~> 3.3) + rdf-trig (~> 3.3) + rdf-trix (~> 3.3) + rdf-turtle (~> 3.3) + rdf-vocab (~> 3.3) + rdf-xsd (~> 3.3) + shacl (~> 0.4) + shex (~> 0.8) + sparql (~> 3.3) + sparql-client (~> 3.3) + yaml-ld (~> 0.0) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - llhttp-ffi (0.4.0) + llhttp-ffi (0.5.0) ffi-compiler (~> 1.0) rake (~> 13.0) - lograge (0.13.0) + logger (1.6.1) + lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) @@ -560,6 +632,7 @@ GEM loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) + lumberjack (1.2.10) mail (2.8.1) mini_mime (>= 0.1.1) net-imap @@ -571,45 +644,56 @@ GEM unf marcel (1.0.4) matrix (0.4.2) - memoist (0.16.2) method_source (1.1.0) - mime-types (3.4.1) + mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) + mime-types-data (3.2024.0903) mini_mime (1.1.5) - mini_portile2 (2.8.7) - minitar (0.9) - minitest (5.24.1) - msgpack (1.6.0) + minitar (1.0.2) + minitest (5.25.1) + msgpack (1.7.2) multi_json (1.15.0) - multi_xml (0.6.0) - multipart-post (2.3.0) - mysql2 (0.5.5) - net-imap (0.4.14) + multi_xml (0.7.1) + bigdecimal (~> 3.1) + multipart-post (2.4.1) + mustermann (2.0.2) + ruby2_keywords (~> 0.0.1) + mutex_m (0.2.0) + mysql2 (0.5.6) + nenv (0.3.0) + net-http (0.4.1) + uri + net-http-persistent (4.0.4) + connection_pool (~> 2.2) + net-imap (0.4.16) date net-protocol - net-ldap (0.18.0) + net-ldap (0.19.0) net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout net-scp (4.0.0) net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) net-smtp (0.5.0) net-protocol - net-ssh (7.0.1) + net-ssh (7.2.3) netrc (0.11.0) nio4r (2.7.3) noid (0.9.0) - noid-rails (3.1.0) - actionpack (>= 5.0.0, < 7.1) + noid-rails (3.2.0) + actionpack (>= 5.0.0, < 8) noid (~> 0.9) - nokogiri (1.16.6) - mini_portile2 (~> 2.8.2) + nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) nom-xml (1.2.0) i18n nokogiri + notiffany (0.1.3) + nenv (~> 0.1) + shellany (~> 0.0) oauth (0.5.14) oauth2 (1.4.11) faraday (>= 0.17.3, < 3.0) @@ -617,50 +701,61 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 4) - okcomputer (1.18.4) - omniauth (2.1.1) + okcomputer (1.18.5) + omniauth (2.1.2) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection omniauth-identity (3.0.9) bcrypt omniauth - omniauth-saml (2.1.0) - omniauth (~> 2.0) - ruby-saml (~> 1.12) + omniauth-saml (2.2.1) + omniauth (~> 2.1) + ruby-saml (~> 1.17) orm_adapter (0.5.0) os (1.1.4) - ostruct (0.5.5) - parallel (1.23.0) - paranoia (2.6.1) - activerecord (>= 5.1, < 7.1) - parser (3.2.1.0) + ostruct (0.6.0) + package_json (0.1.0) + parallel (1.26.3) + paranoia (2.6.4) + activerecord (>= 5.1, < 7.2) + parser (3.3.5.0) ast (~> 2.4.1) - pg (1.5.3) + racc + pg (1.5.8) popper_js (1.16.1) - pretender (0.4.0) - actionpack (>= 5.2) + pretender (0.5.0) + actionpack (>= 6.1) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) pry-byebug (3.10.1) byebug (~> 11.0) pry (>= 0.13, < 0.15) - pry-rails (0.3.9) - pry (>= 0.10.4) + pry-rails (0.3.11) + pry (>= 0.13.0) psych (3.3.4) - public_suffix (5.0.1) + public_suffix (6.0.1) puma (6.4.2) nio4r (~> 2.0) raabro (1.4.0) - racc (1.8.0) + racc (1.8.1) rack (2.2.9) rack-cors (2.0.2) rack (>= 2.0.0) - rack-protection (3.0.5) + rack-linkeddata (3.3.0) + linkeddata (~> 3.3) + rack (>= 2.2, < 4) + rack-rdf (~> 3.3) + rack-protection (2.2.4) rack - rack-proxy (0.7.6) + rack-proxy (0.7.7) rack + rack-rdf (3.3.0) + rack (>= 2.2, < 4) + rdf (~> 3.3) + rack-session (1.0.2) + rack (< 3) rack-test (2.1.0) rack (>= 1.3) rails (7.0.8.4) @@ -701,63 +796,121 @@ GEM rainbow (3.1.1) rake (13.2.1) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) rb-readline (0.5.5) - rdf (3.2.11) + rdf (3.3.2) + bcp47_spec (~> 0.2) + bigdecimal (~> 3.1, >= 3.1.5) + link_header (~> 0.0, >= 0.0.8) + rdf-aggregate-repo (3.3.0) + rdf (~> 3.3) + rdf-hamster-repo (3.3.0) + hamster (~> 3.0) + rdf (~> 3.3) + rdf-isomorphic (3.3.0) + rdf (~> 3.3) + rdf-json (3.3.0) + rdf (~> 3.3) + rdf-ldp (2.1.0) + json-ld (~> 3.2) + ld-patch (~> 3.2) link_header (~> 0.0, >= 0.0.8) - rdf-isomorphic (3.2.1) + rack (~> 2.2) + rack-linkeddata (~> 3.1) rdf (~> 3.2) - rdf-ldp (0.1.0) - deprecation - rdf - rdf-rdfxml (3.2.2) - builder (~> 3.2) + rdf-turtle (~> 3.2) + rdf-vocab (~> 3.2) + sinatra (~> 2.1) + rdf-microdata (3.3.0) htmlentities (~> 4.3) - rdf (~> 3.2) - rdf-xsd (~> 3.2) - rdf-turtle (3.2.1) - ebnf (~> 2.3) - rdf (~> 3.2) - rdf-vocab (3.2.3) - rdf (~> 3.2, >= 3.2.4) - rdf-xsd (3.2.1) - rdf (~> 3.2) + nokogiri (~> 1.15, >= 1.15.4) + rdf (~> 3.3) + rdf-rdfa (~> 3.3) + rdf-xsd (~> 3.3) + rdf-n3 (3.3.0) + ebnf (~> 2.4) + rdf (~> 3.3) + sparql (~> 3.3) + sxp (~> 1.3) + rdf-normalize (0.7.0) + rdf (~> 3.3) + rdf-ordered-repo (3.3.0) + rdf (~> 3.3) + rdf-rdfa (3.3.0) + haml (~> 6.1) + htmlentities (~> 4.3) + rdf (~> 3.3) + rdf-aggregate-repo (~> 3.3) + rdf-vocab (~> 3.3) + rdf-xsd (~> 3.3) + rdf-rdfxml (3.3.0) + builder (~> 3.2, >= 3.2.4) + htmlentities (~> 4.3) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + rdf-reasoner (0.9.0) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + rdf-tabular (3.3.0) + addressable (~> 2.8) + bcp47_spec (~> 0.2) + json-ld (~> 3.3) + rdf (~> 3.3) + rdf-vocab (~> 3.3) + rdf-xsd (~> 3.3) + rdf-trig (3.3.0) + ebnf (~> 2.4) + rdf (~> 3.3) + rdf-turtle (~> 3.3) + rdf-trix (3.3.0) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + rdf-turtle (3.3.0) + ebnf (~> 2.4) + rdf (~> 3.3) + rdf-vocab (3.3.1) + rdf (~> 3.3) + rdf-xsd (3.3.0) + rdf (~> 3.3) rexml (~> 3.2) - react-rails (2.7.1) + react-rails (3.2.1) babel-transpiler (>= 0.7.0) connection_pool execjs railties (>= 3.2) tilt - recaptcha (5.14.0) - redis (4.8.1) - redis-actionpack (5.3.0) + recaptcha (5.17.0) + redis (5.3.0) + redis-client (>= 0.22.0) + redis-actionpack (5.4.0) actionpack (>= 5, < 8) - redis-rack (>= 2.1.0, < 3) + redis-rack (>= 2.1.0, < 4) redis-store (>= 1.1.0, < 2) redis-activesupport (5.3.0) activesupport (>= 3, < 8) redis-store (>= 1.3, < 2) - redis-rack (2.1.4) - rack (>= 2.0.8, < 3) + redis-client (0.22.2) + connection_pool + redis-rack (3.0.0) + rack-session (>= 0.2.0) redis-store (>= 1.2, < 2) redis-rails (5.0.2) redis-actionpack (>= 5.0, < 6) redis-activesupport (>= 5.0, < 6) redis-store (>= 1.2, < 2) - redis-store (1.9.1) - redis (>= 4, < 5) - redlock (1.3.2) - redis (>= 3.0.0, < 6.0) - regexp_parser (2.7.0) + redis-store (1.11.0) + redis (>= 4, < 6) + redlock (2.0.6) + redis-client (>= 0.14.1, < 1.0.0) + regexp_parser (2.9.2) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - request_store (1.5.1) + request_store (1.7.0) rack (>= 1.4) - responders (3.1.0) + responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) rest-client (2.1.0) @@ -766,36 +919,35 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) retriable (3.1.2) - rexml (3.3.1) - strscan - roo (2.10.0) + rexml (3.3.7) + roo (2.10.1) nokogiri (~> 1) rubyzip (>= 1.3.0, < 3.0.0) - rsolr (2.5.0) + rsolr (2.6.0) builder (>= 2.1.2) faraday (>= 0.9, < 3, != 2.0.0) - rspec-core (3.12.1) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.2) + rspec-core (3.13.1) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) + rspec-support (~> 3.13.0) rspec-its (1.3.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.12.3) + rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-rails (6.0.3) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) - rspec-core (~> 3.12) - rspec-expectations (~> 3.12) - rspec-mocks (~> 3.12) - rspec-support (~> 3.12) + rspec-support (~> 3.13.0) + rspec-rails (7.0.1) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) rspec-retry (0.6.2) rspec-core (> 3.3) - rspec-support (3.12.0) + rspec-support (3.13.1) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.28.2) @@ -807,9 +959,9 @@ GEM rubocop-ast (>= 1.17.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.24.1) - parser (>= 3.1.1.0) - rubocop-performance (1.16.0) + rubocop-ast (1.32.3) + parser (>= 3.3.1.0) + rubocop-performance (1.19.1) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) rubocop-rails (2.15.2) @@ -824,7 +976,7 @@ GEM multipart-post oauth2 ruby-progressbar (1.13.0) - ruby-saml (1.15.0) + ruby-saml (1.17.0) nokogiri (>= 1.13.10) rexml ruby2_keywords (0.0.5) @@ -836,38 +988,50 @@ GEM pretender rails (>= 5.2.4.3, < 7.1) sass (3.4.22) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt scanf (1.0.0) scrub_rb (1.0.1) selenium-webdriver (3.142.7) childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) semantic_range (3.0.0) - sequel (5.77.0) + sequel (5.84.0) bigdecimal - shakapacker (7.0.2) + shacl (0.4.1) + json-ld (~> 3.3) + rdf (~> 3.3) + sparql (~> 3.3) + sxp (~> 1.2) + shakapacker (8.0.2) activesupport (>= 5.2) + package_json rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - shoulda-matchers (5.3.0) + shellany (0.0.1) + shex (0.8.0) + ebnf (~> 2.4) + htmlentities (~> 4.3) + json-ld (~> 3.3) + json-ld-preloaded (~> 3.3) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + sparql (~> 3.3) + sxp (~> 1.3) + shoryuken (6.2.1) + aws-sdk-core (>= 2) + concurrent-ruby + thor + shoulda-matchers (6.4.0) activesupport (>= 5.2.0) - sidekiq (6.5.12) - connection_pool (>= 2.2.5, < 3) + sidekiq (6.5.5) + connection_pool (>= 2.2.2) rack (~> 2.0) - redis (>= 4.5.0, < 5) - sidekiq-cron (1.10.1) + redis (>= 4.5.0) + sidekiq-cron (1.12.0) fugit (~> 1.8) globalid (>= 1.0.1) sidekiq (>= 6) - signet (0.17.0) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -876,8 +1040,13 @@ GEM docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) + sinatra (2.2.4) + mustermann (~> 2.0) + rack (~> 2.2) + rack-protection (= 2.2.4) + tilt (~> 2.0) slop (4.10.1) solr_wrapper (4.0.2) http @@ -890,63 +1059,75 @@ GEM nokogiri stomp xml-simple + sparql (3.3.0) + builder (~> 3.2, >= 3.2.4) + ebnf (~> 2.4) + logger (~> 1.5) + rdf (~> 3.3) + rdf-aggregate-repo (~> 3.3) + rdf-xsd (~> 3.3) + sparql-client (~> 3.3) + sxp (~> 1.3) + sparql-client (3.3.0) + net-http-persistent (~> 4.0, >= 4.0.2) + rdf (~> 3.3) speedy-af (0.3.0) active-fedora (>= 11.0.0) activesupport (> 5.2) - sprockets (3.7.2) + spoon (0.0.6) + ffi + sprockets (3.7.4) + base64 concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-es6 (0.9.2) babel-source (>= 5.8.11) babel-transpiler sprockets (>= 3.0.0) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) - sqlite3 (1.6.3) - mini_portile2 (~> 2.8.0) - sshkit (1.21.3) + sqlite3 (1.7.3-x86_64-linux) + sshkit (1.23.1) + base64 net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) + ostruct stomp (1.4.10) - strscan (3.1.0) suo (0.4.0) dalli msgpack redis - sxp (1.2.3) + sxp (1.3.0) matrix (~> 0.4) - rdf (~> 3.2) - terser (1.2.0) + rdf (~> 3.3) + temple (0.10.3) + terser (1.2.3) execjs (>= 0.3.0, < 3) - thor (1.3.1) - tilt (2.0.11) + thor (1.3.2) + tilt (2.4.0) timeout (0.4.1) trailblazer-option (0.1.2) twitter-typeahead-rails (0.11.1.pre.corejavascript) actionpack (>= 3.1) jquery-rails railties (>= 3.1) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.0.15) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (2.4.2) - unicode-types (1.8.0) - user_agent_parser (2.14.0) - view_component (2.83.0) + unf (0.2.0) + unicode-display_width (2.6.0) + unicode-types (1.10.0) + uri (0.13.1) + user_agent_parser (2.18.0) + view_component (3.14.0) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) method_source (~> 1.0) warden (1.2.9) rack (>= 2.0.9) wavefile (1.0.1) - web-console (4.2.0) + web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) @@ -955,11 +1136,10 @@ GEM nokogiri (~> 1.6) rubyzip (~> 1.0) selenium-webdriver (~> 3.0) - webmock (3.18.1) + webmock (3.23.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.8.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -968,14 +1148,19 @@ GEM rexml xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.16) + yaml-ld (0.0.3) + json-ld (~> 3.3) + psych (>= 3.3) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + zeitwerk (2.6.18) zk (1.10.0) zookeeper (~> 1.5.0) zookeeper (1.5.5) zoom (0.5.0) PLATFORMS - ruby + x86_64-linux DEPENDENCIES about_page! @@ -994,11 +1179,16 @@ DEPENDENCIES avalon-workflow! aws-partitions aws-sdk-cloudfront + aws-sdk-cloudwatchevents + aws-sdk-cloudwatchlogs aws-sdk-elastictranscoder + aws-sdk-lambda + aws-sdk-mediaconvert aws-sdk-rails aws-sdk-s3 aws-sdk-ses aws-sdk-sqs + aws-sdk-ssm aws-sigv4 bixby blacklight (~> 7.25) @@ -1027,14 +1217,17 @@ DEPENDENCIES edtf (>= 3.1.1) email_spec equivalent-xml + ezid-client factory_bot_rails fakefs faker fastimage fcrepo_wrapper font-awesome-rails - google-analytics-rails (= 1.1.0) + guard-process + guard-puma hashdiff (>= 1.0) + honeybadger (~> 4.0) hooks httpx hydra-head (~> 12.0) @@ -1060,6 +1253,7 @@ DEPENDENCIES omniauth (~> 2.0) omniauth-identity (>= 2.0.0) omniauth-lti! + omniauth-nusso! omniauth-saml (~> 2.0) parallel pg @@ -1089,6 +1283,7 @@ DEPENDENCIES selenium-webdriver sequel shakapacker + shoryuken shoulda-matchers sidekiq (~> 6.2) sidekiq-cron (~> 1.9) @@ -1097,7 +1292,7 @@ DEPENDENCIES speedy-af (~> 0.3) sprockets (~> 3.7.2) sprockets-es6 - sqlite3 + sqlite3 (~> 1.4) terser twitter-typeahead-rails (= 0.11.1.pre.corejavascript) wavefile (~> 1.0.1) @@ -1109,4 +1304,4 @@ DEPENDENCIES zoom BUNDLED WITH - 2.3.6 + 2.4.19 diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000000..e8b1f2d3c6 --- /dev/null +++ b/Guardfile @@ -0,0 +1,30 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +## Uncomment and set this to only include directories you want to watch +# directories %w(app lib config test spec features) \ +# .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")} + +## Note: if you are using the `directories` clause above and you are not +## watching the project directory ('.'), then you will want to move +## the Guardfile to a watched dir and symlink it back, e.g. +# +# $ mkdir config +# $ mv Guardfile config/ +# $ ln -s config/Guardfile . +# +# and, you'll have to watch "config/Guardfile" instead of "Guardfile" + +group :webapp do + guard 'puma' do + watch('Gemfile.lock') + watch(%r{^app|config|lib|api/.*}) + end +end + +group :worker do + guard 'process', name: 'sidekiq', command: 'sidekiq' do + watch('Gemfile.lock') + watch(%r{^app|config|lib|api/.*}) + end +end diff --git a/NUL_AVR_DEVELOPMENT.md b/NUL_AVR_DEVELOPMENT.md new file mode 100644 index 0000000000..11fc3ffebe --- /dev/null +++ b/NUL_AVR_DEVELOPMENT.md @@ -0,0 +1,74 @@ +## Running AVR in Development Mode + +This document explains how to run AVR in the [remote development environment](http://docs.rdc.library.northwestern.edu/2._Developer_Guides/Environment_and_Tools/Remote-Development-Environment-FAQ/). It supersedes the development instructions in [README.md](README.md). + +### One-Time Setup + +The first time you prepare to run AVR on a new development system, there are a few prerequisites you need to install, update, and configure. + +* `cd` to the Avalon/AVR working directory +* `asdf install ruby 3.2.4` +* `asdf local ruby 3.2.4` +* `gem install --no-doc bundler` +* `app-environment avr` +* Create `config/settings/development.local.yml`: + ``` + encoding: + media_convert: + configuration: + mapping: + '720': high + '540': low + options: + avalon: + media_type: video + outputs: + - preset: avr-video-medium + modifier: "-720" + - preset: avr-video-low + modifier: "-540" + fullaudio: + media_type: audio + outputs: + - preset: avr-audio-high + modifier: "-high" + - preset: avr-audio-medium + modifier: "-medium" + ``` +* `bundle config set --local with puma:aws:postgres:ssl_dev:ezid` +* `bundle config set --local build.zookeeper --with-cflags=-Wno-error=format-overflow` +* `bundle install` + +### Initializing/Clearing the Development Stack + +```shell +bundle exec rake avr:reset +``` + +or for the test environment: + +```shell +RAILS_ENV=test bundle exec rake avr:reset +``` + +### Starting the server and background workers + +```shell +bundle exec guard -i +``` + +### Using AVR + +* Access the local site via `https://DEV_PREFIX.dev.rdc.library.northwestern.edu:3001/` +* After logging in for the first time, open a new window (no need to shut down the server) and run `bundle exec rake avalon:user:admin`. Enter your northwestern email address to grant yourself admin rights. +* Refresh your browser window and make sure you see the `Manage` menu in the top nav bar + +### Running rspec tests + +**Note: The test suite is currently not usable in the dev environment. These instructions will be updated if necessary once that's worked out.** + +```shell +bundle exec rspec -cf doc spec +``` + +**Note:** You might want to limit yourself to running whatever tests are relevant to the changes you've just made by replacing the `spec` at the end with the path to a specific test file or directory. The entire suite takes more than 45 minutes to run. diff --git a/app/assets/stylesheets/avalon/_mediaelement.scss b/app/assets/stylesheets/avalon/_mediaelement.scss new file mode 100644 index 0000000000..e373580a19 --- /dev/null +++ b/app/assets/stylesheets/avalon/_mediaelement.scss @@ -0,0 +1,37 @@ +/* + * Copyright 2011-2020, The Trustees of Indiana University and Northwestern + * University. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * --- END LICENSE_HEADER BLOCK --- +*/ + +.mejs-overlay-loading { + border-radius: 50% !important; +} + +.me-cannotplay { + @extend .alert; + @extend .alert-danger; + text-align: center; + visibility: visible !important; +} + +.mejs-time-clip { + background: linear-gradient(#ddd, #ddd); + opacity: 0.3; + height: 14px !important; + top: -3px !important; +} + +.mejs__container { + z-index: 1000; +} diff --git a/app/assets/stylesheets/datatables.css b/app/assets/stylesheets/datatables.css new file mode 100644 index 0000000000..fa77d93f17 --- /dev/null +++ b/app/assets/stylesheets/datatables.css @@ -0,0 +1,96 @@ +/* + * Copyright 2011-2020, The Trustees of Indiana University and Northwestern + * University. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * --- END LICENSE_HEADER BLOCK --- +*/ + +/* +*= require datatables/dataTables.bootstrap +* +*optional add '=' enable +* require datatables/extensions/AutoFill/autoFill.bootstrap +* require datatables/extensions/Buttons/buttons.bootstrap +* require datatables/extensions/ColReorder/colReorder.bootstrap +* require datatables/extensions/FixedColumns/fixedColumns.bootstrap +* require datatables/extensions/FixedHeader/fixedHeader.bootstrap +* require datatables/extensions/KeyTable/keyTable.bootstrap +* require datatables/extensions/Responsive/responsive.bootstrap +* require datatables/extensions/RowReorder/rowReorder.bootstrap +* require datatables/extensions/Scroller/scroller.bootstrap +* require datatables/extensions/Select/select.bootstrap +*/ + +table.dataTable { + border-collapse: collapse !important; + border-spacing: 0; +} + +table.dataTable thead tr th, +table.dataTable tbody tr td { + padding: 10px; +} + +table.dataTable a { + text-decoration: underline; +} + +table.dataTable a.btn { + text-decoration: none; +} + +table.dataTable thead .sorting, +table.dataTable thead .sorting_asc, +table.dataTable thead .sorting_desc, +table.dataTable thead .sorting_asc_disabled, +table.dataTable thead .sorting_desc_disabled { + white-space: nowrap; + cursor: pointer; + *cursor: hand; +} + +table.dataTable thead .sorting:after, +table.dataTable thead .sorting_asc:after, +table.dataTable thead .sorting_desc:after, +table.dataTable thead .sorting_asc_disabled:after, +table.dataTable thead .sorting_desc_disabled:after { + position: relative; + left: 5px; + display: inline-block; + content: ''; + width: 1em; + height: 1em; + opacity: 1; + background-repeat: no-repeat; + background-position: center right; +} + +table.dataTable thead .sorting:after { + background-image: url("datatables/sort_both.png"); + bottom: -1px; +} +table.dataTable thead .sorting_asc:after { + background-image: url("datatables/sort_asc.png"); + bottom: -4px; +} +table.dataTable thead .sorting_desc:after { + background-image: url("datatables/sort_desc.png"); + bottom: 1px; +} +table.dataTable thead .sorting_asc_disabled:after { + background-image: url("datatables/sort_asc_disabled.png"); + bottom: -4px; +} +table.dataTable thead .sorting_desc_disabled:after { + background-image: url("datatables/sort_desc_disabled.png"); + bottom: 1px; +} diff --git a/app/assets/stylesheets/mejs4/mediaelement-common-styles.scss b/app/assets/stylesheets/mejs4/mediaelement-common-styles.scss new file mode 100644 index 0000000000..d1bfd6afae --- /dev/null +++ b/app/assets/stylesheets/mejs4/mediaelement-common-styles.scss @@ -0,0 +1,177 @@ +/* + * Copyright 2011-2020, The Trustees of Indiana University and Northwestern + * University. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * --- END LICENSE_HEADER BLOCK --- +*/ + +$avalon-primary-color: #84a791; + +// Generic class applied to the Add To Playlist form elements +.mejs-form-wrapper { + max-width: 870px; + background: #696667; + color: white; + padding: 0 20px; + border: 1pt solid #353536; + border-bottom-right-radius: 6pt; + border-bottom-left-radius: 6pt; + + input { + height: 28px; + padding-top: 7px; + } + + select, input, textarea { + width: 100%; + color: black; + } + + textarea { + resize: vertical; + } + + #select2-post_playlist_id-container { + color: black; + text-align: left; + } + +} + +.mejs-form-alert { + width: 100%; + border-radius: 0; + margin-bottom: 0; + + a { + font-weight: bold; + text-decoration: underline; + } + // TODO: Add a close button to alerts? + .close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + text-shadow: 0 1px 0 white; + opacity: 0.2; + } +} + +// MEJS4 custom highlight for a section within the MEJS time rail element +.mejs-highlight-clip { + background: linear-gradient($avalon-primary-color, $avalon-primary-color); + opacity: 0.55; + height: 24px; + top: -7px; + display: block; + position: relative; + z-index: -1; +} + +// Loading spinner +@keyframes spinner { + to {transform: rotate(360deg);} +} +.spinner { + opacity: .6; + + &::before { + content: ''; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 50%; + width: 100px; + height: 100px; + margin-top: -50px; + margin-left: -50px; + border-radius: 50%; + border: 10px solid #fff; + border-top-color: #84a791; + animation: spinner .8s linear infinite; + } +} + +// Player Controls +.mejs__button { + width: 30px; +} + +.mejs__button > button { + background: transparent image-url('/assets/mejs4_icons-01.svg'); + background-repeat: none; + background-size: 170px 40px; +} + +.mejs__play > button { + background-position: 0px 0px; +} + +.mejs__pause > button { + background-position: -20px 0px; +} + +.mejs__fullscreen-button > button { + background-position: -60px 0px; +} + +.mejs__unfullscreen > button { + background-position: -80px 0px; +} + +.mejs__mute, .mejs__unmute { + width: 36px; +} + +.mejs__mute > button { + background-position: 0px -20px; + width: 25px; +} + +.mejs__unmute > button { + background-position: -25px -20px; + width: 25px; +} + +.mejs__qualities-button > button { + background-position: -40px 0px; + text-indent: 200px; //hide standard text off to the side + margin: 10px 6px; + width: 20px; +} + +.mejs__captions-button > button { + width: 22px; + background-position: -140px -20px; +} + +.mejs__captions-enabled > button { + border-bottom: 2px solid red; + padding: 11px; +} + +.mejs__hd-toggle-button { + width: 35px; +} + +.mejs__hd-toggle-button > button { + width: 27px; + background-position: -141px -2px; + filter: brightness(60%); + -webkit-filter: brightness(60%); +} + +.mejs__hdtoggle-on > button { + filter: none; + -webkit-filter: none; +} diff --git a/app/assets/stylesheets/mejs4/mejs4_link_back.scss b/app/assets/stylesheets/mejs4/mejs4_link_back.scss new file mode 100644 index 0000000000..392d2836d1 --- /dev/null +++ b/app/assets/stylesheets/mejs4/mejs4_link_back.scss @@ -0,0 +1,40 @@ +/* + * Copyright 2011-2020, The Trustees of Indiana University and Northwestern + * University. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * --- END LICENSE_HEADER BLOCK --- +*/ + +/** + * Stylesheet for Avalon custom MediaElement 4 plugin: "Title Link". + */ + +.mejs__title-link { + width: 100%; + z-index: 999; + position: absolute; + top: 0; + left: 0; + background: rgba(0, 0, 0, 0.35); +} + +#mejs-title-link a { + color: white; + display: block; + font-size: 12pt; + margin: 10px 15px; +} + +.mejs-link-back-button > button, +.mejs__link-back-button > button { + background-position: -120px -20px; + } diff --git a/app/assets/stylesheets/mejs4/mejs4_plugin_add_marker_to_playlist.scss b/app/assets/stylesheets/mejs4/mejs4_plugin_add_marker_to_playlist.scss new file mode 100644 index 0000000000..e62c39c245 --- /dev/null +++ b/app/assets/stylesheets/mejs4/mejs4_plugin_add_marker_to_playlist.scss @@ -0,0 +1,139 @@ +/* + * Copyright 2011-2020, The Trustees of Indiana University and Northwestern + * University. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * --- END LICENSE_HEADER BLOCK --- +*/ + +/** + * Stylesheet for Avalon custom MediaElement 4 plugin: "Add Marker To Playlist". + */ + +.mejs__add-marker-to-playlist-button > button, +.mejs-add-marker-to-playlist-button > button { + background-position: -100px 0px; + width: 20px; +} + +#markers { + table { + tr { + .display-item { + display: block; + } + button.display-item { + display: inline-block; + } + .edit-item { + display: none; + } + &.is-editing { + .display-item { + display: none; + } + .edit-item { + display: block; + } + button.edit-item { + display: inline-block; + } + } + } + th { + &:nth-child(1) { + width: 50%; + } + &:nth-child(2) { + width: 25%; + } + } + td { + // The Edit/Delte buttons column + &:last-of-type { + position: relative; + + .popover { + min-width: 165px; + } + } + } + } +} + +.scrubber-marker { + width: 1em !important; + top: 0; + transform: translate(-50%, 0%); + position: absolute; +} + +.scrubber-marker:hover { + cursor: pointer; +} + +.mejs__time-rail-marker, +.mejs__time-current-marker, +.mejs__time-float-marker, +.mejs__time-float-current-marker, +.mejs__time-float-corner-marker { + border-radius: 2px; + display: block; + height: 10px; + position: absolute; +} + +.mejs__time-float-marker { + background: #eee; + border: solid 1px #333; + bottom: 20px; + color: #111; + display: none; + height: auto !important; + margin-bottom: 9px; + text-align: center; + -webkit-transform: translateX(-50%); + -ms-transform: translateX(-50%); + transform: translateX(-50%); + width: 100px !important; +} + +.mejs__time-float-current-marker { + display: block; + position: relative !important; + height: auto !important; + width: 92px !important; + left: 0; + margin: 4px; + text-align: center; +} + +.mejs__time-float-corner-marker { + border: solid 5px #eee; + border-color: #eee transparent transparent; + border-radius: 0; + display: block; + height: 0; + left: 50%; + line-height: 0; + position: absolute; + top: 100%; + -webkit-transform: translateX(-50%); + -ms-transform: translateX(-50%); + transform: translateX(-50%); + width: 0; +} + +.mejs__time-rail-marker { + margin: 5px 0 0; + width: 100%; + top: 20px; +} diff --git a/app/assets/stylesheets/mejs4/mejs4_plugin_add_to_playlist.scss b/app/assets/stylesheets/mejs4/mejs4_plugin_add_to_playlist.scss new file mode 100644 index 0000000000..ba6b1ed695 --- /dev/null +++ b/app/assets/stylesheets/mejs4/mejs4_plugin_add_to_playlist.scss @@ -0,0 +1,40 @@ +/* + * Copyright 2011-2020, The Trustees of Indiana University and Northwestern + * University. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * --- END LICENSE_HEADER BLOCK --- +*/ + +/** + * Stylesheet for Avalon custom MediaElement 4 plugin: "Add To Playlist". + */ + +$avalon-primary-color: #84a791; + +// Control button on MEJS player +.mejs-add-to-playlist-button > button, +.mejs__add-to-playlist-button > button { + background-position: -50px -20px; + width: 25px; +} + +.add_playlist_item_playlists_no_playlists_message { + a { + color: $avalon-primary-color !important; + } +} + +.playlist-visibility-form-group { + > label:first-of-type { + display: block; + } +} diff --git a/app/assets/stylesheets/mejs4/mejs4_plugin_create_thumbnail.scss b/app/assets/stylesheets/mejs4/mejs4_plugin_create_thumbnail.scss new file mode 100644 index 0000000000..a68f8602ec --- /dev/null +++ b/app/assets/stylesheets/mejs4/mejs4_plugin_create_thumbnail.scss @@ -0,0 +1,30 @@ +/* + * Copyright 2011-2020, The Trustees of Indiana University and Northwestern + * University. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * --- END LICENSE_HEADER BLOCK --- +*/ + +/** + * Stylesheet for Avalon custom MediaElement 4 plugin: "Create Thumbnail". + */ + +.mejs-create-thumbnail-button > button, +.mejs__create-thumbnail-button > button { + background-position: -120px 0px; + } + +#create-thumbnail-modal { + .alert { + padding-right: 35px; + } +} diff --git a/app/assets/stylesheets/mejs4/mejs4_plugin_track_scrubber.scss b/app/assets/stylesheets/mejs4/mejs4_plugin_track_scrubber.scss new file mode 100644 index 0000000000..4b769cf850 --- /dev/null +++ b/app/assets/stylesheets/mejs4/mejs4_plugin_track_scrubber.scss @@ -0,0 +1,180 @@ +/* + * Copyright 2011-2020, The Trustees of Indiana University and Northwestern + * University. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * --- END LICENSE_HEADER BLOCK --- +*/ + +/** + * Stylesheet for Avalon custom MediaElement 4 plugin: "Track Scrubber". + */ + +.mejs-track-scrubber-button, +.mejs__track-scrubber-button { + > button { + background-position: -98px -20px; + } + + &.track-scrubber-hide { + > button { + background-position: -77px -20px; + } + } + } + +.mejs-time-clip { + background: linear-gradient(#DDD,#DDD); + opacity: .3; + height: 14px !important; + top: -3px !important; +} + +.track_scrubber { + max-width: 870px; + background: #696667; + color: white; + padding: 0pt 10pt 10pt 10pt; + border: 1pt solid #353536; + list-style-type: none; + margin: 0; + padding: 0; + bottom: 0; + left: 0; + height: 36px; + width: 100%; + display: flex; + align-items: flex-end; + + &.hidden { + display: none; + } + + div { + list-style-type: none; + background-image: none; + margin: 0; + padding: 0; + width: 26px; + height: 26px; + font-size: 11px; + line-height: 11px; + font-family: Helvetica, Arial; + border: 0; + } + + > div { + display: flex; + flex-direction: row; + justify-content: flex-start; + } + + .mejs-time { + color: #fff; + width: 41.5px; + overflow: hidden; + padding: 2px 5px; + text-align: center; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; + span { + color: #fff; + font-size: 11px; + line-height: 12px; + display: block; + margin: 6px 2px 0 0px; + width: auto; + } + } + /* Start: Progress Bar */ + div.track-mejs-time-rail { + direction: ltr; + width: 80%; + span { + display: block; + position: absolute; + height: 10px; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 1px; + cursor: pointer; + } + .track-mejs-time-total { + height: 20px; + background: #333; + background: rgba(50,50,50,0.8); + background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(30,30,30,0.8)), to(rgba(60,60,60,0.8))); + background: -webkit-linear-gradient(top, rgba(30,30,30,0.8), rgba(60,60,60,0.8)); + background: -moz-linear-gradient(top, rgba(30,30,30,0.8), rgba(60,60,60,0.8)); + background: -o-linear-gradient(top, rgba(30,30,30,0.8), rgba(60,60,60,0.8)); + background: -ms-linear-gradient(top, rgba(30,30,30,0.8), rgba(60,60,60,0.8)); + background: linear-gradient(rgba(30,30,30,0.8), rgba(60,60,60,0.8)); + } + .track-mejs-time-current { + background: #84a791; + opacity: .8; + width: 0; + height: 20px; + } + .track-mejs-time-handle { + display: none; + position: absolute; + margin: 0; + width: 10px; + background: #fff; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + cursor: pointer; + border: solid 2px #333; + top: -2px; + text-align: center; + } + .track-mejs-time-float { + position: absolute; + display: none; + background: #eee; + width: 49px; + float: left; + height: 17px; + border: solid 1px #333; + top: -26px; + margin-left: -18px; + text-align: center; + color: #111; + z-index: 1001; + } + .track-mejs-time-float-current { + margin-top: 1.5px; + width: 100%; + float: left; + left: 0; + } + .track-mejs-time-float-corner { + position: absolute; + display: block; + width: 0; + height: 0; + line-height: 0; + border: solid 5px #eee; + border-color: #eee transparent transparent transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + top: 15px; + left: 13px; + } + } + .track-mejs-time-rail:hover .track-mejs-time-handle { + visibility:visible; + } + } diff --git a/app/assets/stylesheets/mejs4_player.scss b/app/assets/stylesheets/mejs4_player.scss new file mode 100644 index 0000000000..0bde43aafb --- /dev/null +++ b/app/assets/stylesheets/mejs4_player.scss @@ -0,0 +1,27 @@ +/* + * Copyright 2011-2020, The Trustees of Indiana University and Northwestern + * University. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * --- END LICENSE_HEADER BLOCK --- +*/ + +/* + *= require mediaelement/mediaelementplayer.scss + *= require mediaelement/plugins/quality.css + *= require mejs4/mediaelement-common-styles.scss + *= require mejs4/mejs4_plugin_add_to_playlist.scss + *= require mejs4/mejs4_plugin_add_marker_to_playlist.scss + *= require mejs4/mejs4_plugin_create_thumbnail.scss + *= require mejs4/mejs4_plugin_track_scrubber.scss + *= require mejs4/mejs4_link_back.scss + *= require mediaelement/plugins/seekmedia.css + */ diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index aa8f49d93f..b4019c33db 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -105,7 +105,7 @@ def items def create @collection = Admin::Collection.create(collection_params.merge(managers: [current_user.user_key])) if @collection.persisted? - User.where(Devise.authentication_keys.first => [Avalon::RoleControls.users('administrator')].flatten).each do |admin_user| + User.find_by_devise_authentication_keys([Avalon::RoleControls.users('administrator')].flatten).each do |admin_user| NotificationsMailer.new_collection( creator_id: current_user.id, collection_id: @collection.id, @@ -176,7 +176,7 @@ def update saved = @collection.save if saved if name_changed - User.where(Devise.authentication_keys.first => [Avalon::RoleControls.users('administrator')].flatten).each do |admin_user| + User.find_by_devise_authentication_keys([Avalon::RoleControls.users('administrator')].flatten).each do |admin_user| NotificationsMailer.update_collection( updater_id: current_user.id, collection_id: @collection.id, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d1893db102..a0b3ce8770 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -53,11 +53,9 @@ def mejs def rewrite_v4_ids params.permit! - query_result = ActiveFedora::SolrService.query(%{identifier_ssim:"#{params[:id]}"}, rows: 1, fl: 'id') - - raise ActiveFedora::ObjectNotFoundError if query_result.empty? - - new_id = query_result.first['id'] + id_record = ActiveFedora::SolrService.query(%{identifier_ssim:"#{params[:id]}"}, rows: 1, fl: 'id').first + return if id_record.nil? + new_id = id_record['id'] new_content_id = params[:content] ? ActiveFedora::SolrService.query(%{identifier_ssim:"#{params[:content]}"}, rows: 1, fl: 'id').first['id'] : nil redirect_to(url_for(params.merge(id: new_id, content: new_content_id))) end @@ -225,7 +223,19 @@ def fetch_object(id) obj || GlobalID::Locator.locate(id) end + def maybe_redirect + return unless params[:id].present? + redirect = Redirect.find_by(id: params[:id]) + return unless redirect.present? + + redirect_target = embed_request? ? redirect.embed_target : redirect.item_target + redirect_to(redirect_target) + end + private + def embed_request? + request.url =~ %r{master_files/.+/embed} + end def remove_zero_width_chars # params is a ActionController::Parameters diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index 3371bbb4db..ff25505b23 100644 --- a/app/controllers/catalog_controller.rb +++ b/app/controllers/catalog_controller.rb @@ -19,9 +19,12 @@ class CatalogController < ApplicationController include Hydra::Catalog include Hydra::MultiplePolicyAwareAccessControlsEnforcement - include BlacklightHelperReloadFix + # include BlacklightHelperReloadFix + + before_action :redirect_specific_collection_facets, only: :index # These before_actions apply the hydra access controls + before_action :block_invalid_sort_params, only: :index before_action :enforce_show_permissions, only: :show before_action :load_home_page_collections, only: :index, if: proc { helpers.current_page? root_path } @@ -96,7 +99,7 @@ class CatalogController < ApplicationController restricted: { label: "Authenticated", fq: "has_model_ssim:MediaObject AND read_access_group_ssim:#{Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_AUTHENTICATED}" }, private: { label: "Private", fq: "has_model_ssim:MediaObject AND NOT read_access_group_ssim:#{Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_PUBLIC} AND NOT read_access_group_ssim:#{Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_AUTHENTICATED}" } } - config.add_facet_field 'read_access_virtual_group_ssim', label: 'External Group', limit: 5, if: Proc.new {|context, config, opts| context.current_ability.can? :create, MediaObject}, group: "workflow", helper_method: :vgroup_display + config.add_facet_field 'read_access_virtual_group_ssim', label: 'Course', limit: 5, if: Proc.new {|context, config, opts| context.current_ability.can? :create, MediaObject}, group: "workflow", helper_method: :vgroup_display config.add_facet_field 'date_digitized_ssim', label: 'Date Digitized', limit: 5, if: Proc.new {|context, config, opts| context.current_ability.can? :create, MediaObject}, group: "workflow"#, partial: 'blacklight/hierarchy/facet_hierarchy' config.add_facet_field 'date_ingested_ssim', label: 'Date Ingested', limit: 5, if: Proc.new {|context, config, opts| context.current_ability.can? :create, MediaObject}, group: "workflow" config.add_facet_field 'has_captions_bsi', label: 'Has Captions', if: Proc.new {|context, config, opts| context.current_ability.can? :create, MediaObject}, group: "workflow", helper_method: :display_has_caption_or_transcript @@ -204,6 +207,20 @@ class CatalogController < ApplicationController end private + def redirect_specific_collection_facets + collection = params.fetch(:f, {}).fetch(:collection_ssim, []).first + return unless collection.present? + redirect = Redirect.find_by(id: collection) + redirect_to(redirect.item_target) if redirect.present? + end + + def block_invalid_sort_params + sort_val = params[:sort] + return unless sort_val + if !blacklight_config.sort_fields.has_key?(sort_val) + render plain: "Requested illegal sort val: #{sort_val}", status: :bad_request + end + end def load_home_page_collections featured_collections = Settings.home_page&.featured_collections diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index b137efa597..d3e7e156f1 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -13,6 +13,7 @@ # --- END LICENSE_HEADER BLOCK --- class CollectionsController < CatalogController + before_action :maybe_redirect, only: [:show] skip_before_action :enforce_show_permissions, only: :show def index diff --git a/app/controllers/master_files_controller.rb b/app/controllers/master_files_controller.rb index 1589b5d0ca..0e378155b0 100644 --- a/app/controllers/master_files_controller.rb +++ b/app/controllers/master_files_controller.rb @@ -21,6 +21,7 @@ class MasterFilesController < ApplicationController # include Avalon::Controller::ControllerBehavior include NoidValidator + before_action :maybe_redirect, only: [:show, :embed] before_action :authenticate_user!, :only => [:create] before_action :set_masterfile_proxy, except: [:create, :oembed, :attach_structure, :delete_structure, :destroy, :update, :set_structure] before_action :set_masterfile, only: [:attach_structure, :delete_structure, :destroy, :update, :set_structure] @@ -262,11 +263,14 @@ def hls_manifest end else return head :unauthorized if cannot?(:read, @master_file) - @hls_streams = if quality == "auto" - gather_hls_streams(@master_file) - else - hls_stream(@master_file, quality) - end + stream = hls_stream(@master_file, quality).first + case stream + when nil + raise ActionController::RoutingError.new('Not Found') unless quality == 'auto' + @hls_streams = gather_hls_streams(@master_file) + else + redirect_to(stream[:url]) + end end end diff --git a/app/controllers/media_objects_controller.rb b/app/controllers/media_objects_controller.rb index 0d6a36c329..b697b87278 100644 --- a/app/controllers/media_objects_controller.rb +++ b/app/controllers/media_objects_controller.rb @@ -23,6 +23,7 @@ class MediaObjectsController < ApplicationController include NoidValidator include SecurityHelper + before_action :maybe_redirect, only: [:show] before_action :authenticate_user!, except: [:show, :set_session_quality, :show_stream_details, :manifest] before_action :load_resource, except: [:create, :destroy, :update_status, :set_session_quality, :tree, :deliver_content, :confirm_remove, :show_stream_details, :add_to_playlist, :intercom_collections, :manifest, :move_preview, :edit, :update, :json_update] load_and_authorize_resource except: [:create, :destroy, :update_status, :set_session_quality, :tree, :deliver_content, :confirm_remove, :show_stream_details, :add_to_playlist, :intercom_collections, :manifest, :move_preview, :show_progress] @@ -150,6 +151,10 @@ def add_to_playlist # POST /media_objects def create + unless (api_params[:collection_id].present?) + render json: { errors: ["New media object must have a valid collection_id"] }, status: 422 + return + end @media_object = MediaObjectsController.initialize_media_object(user_key) # Preset the workflow to the last workflow step to ensure validators run @media_object.workflow.last_completed_step = HYDRANT_STEPS.last.step @@ -174,7 +179,6 @@ def update_media_object @media_object.collection = collection end - @media_object.avalon_uploader = 'REST API' populate_from_catalog = (!!api_params[:import_bib_record] && media_object_parameters[:bibliographic_id].present?) @@ -464,7 +468,10 @@ def manifest authorize! :read, @media_object stream_info_hash = secure_stream_infos(master_file_presenters, [@media_object]) - canvas_presenters = master_file_presenters.collect { |mf| IiifCanvasPresenter.new(master_file: mf, stream_info: stream_info_hash[mf.id]) } + canvas_presenters = master_file_presenters.collect do |mf| + stream_info = secure_streams(mf.stream_details, @media_object.id) + IiifCanvasPresenter.new(master_file: mf, stream_info: stream_info) + end presenter = IiifManifestPresenter.new(media_object: @media_object, master_files: canvas_presenters, lending_enabled: lending_enabled?(@media_object)) manifest = IIIFManifest::V3::ManifestFactory.new(presenter).to_h diff --git a/app/controllers/objects_controller.rb b/app/controllers/objects_controller.rb index 3a0834fc9a..b6b7899866 100644 --- a/app/controllers/objects_controller.rb +++ b/app/controllers/objects_controller.rb @@ -13,6 +13,8 @@ # --- END LICENSE_HEADER BLOCK --- class ObjectsController < ApplicationController + before_action :maybe_redirect, only: [:show] + def show obj = fetch_object params[:id] if obj.blank? diff --git a/app/controllers/samvera/persona/users_controller.rb b/app/controllers/samvera/persona/users_controller.rb index 50dc20087b..f21fd786f4 100644 --- a/app/controllers/samvera/persona/users_controller.rb +++ b/app/controllers/samvera/persona/users_controller.rb @@ -146,14 +146,14 @@ def impersonate user = User.find(params[:id]) impersonate_user(user) # Recalculate user_session[:virtual_groups] - user_session[:virtual_groups] = current_user.ldap_groups + user_session[:virtual_groups] = current_user.virtual_groups redirect_to main_app.root_path end def stop_impersonating stop_impersonating_user # Recalculate user_session[:virtual_groups] - user_session[:virtual_groups] = current_user.ldap_groups + user_session[:virtual_groups] = current_user.virtual_groups redirect_to main_app.persona_users_path, notice: t('.become.over') end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index a87d5229e6..b7ed6db974 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -39,7 +39,7 @@ def after_omniauth_failure_path_for(scope) end def action_missing(sym, *args, &block) - logger.debug "Attempting to find user with #{sym.to_s} strategy" + logger.info "Attempting to find user with #{sym.to_s} strategy" find_user(sym.to_s) end @@ -51,18 +51,17 @@ def find_user(auth_type) auth_type = auth_type.downcase find_method = "find_for_#{auth_type}".to_sym find_method = :find_for_generic unless User.respond_to?(find_method) - logger.debug "#{auth_type} :: #{current_user.inspect}" - @user = User.send(find_method,request.env["omniauth.auth"], current_user) + omniauth_struct = request.env["omniauth.auth"] + logger.info "#{auth_type} :: #{current_user.inspect} :: #{omniauth_struct.inspect}" + @user = User.send(find_method, omniauth_struct, current_user) if @user.persisted? flash[:success] = I18n.t "devise.omniauth_callbacks.success", :kind => auth_type sign_in @user, :event => :authentication - user_session[:virtual_groups] = @user.ldap_groups + user_session[:virtual_groups] = @user.virtual_groups user_session[:full_login] = true if auth_type == 'lti' - user_session[:lti_group] = request.env["omniauth.auth"].extra.context_id - user_session[:virtual_groups] += [user_session[:lti_group]] - user_session[:full_login] = false + user_session[:lti_group] = omniauth_struct.extra.context_id end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2f3e8e3c7f..a2e3d09ba9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -22,24 +22,45 @@ def release_text "#{application_name} #{t(:release_label)} #{Avalon::VERSION}" end + def force_https(uri) + uri.scheme = 'https' unless Rails.env.test? + uri + end + + def https_link + link_uri = URI(yield) + link_uri = force_https(link_uri) + link_uri.to_s + rescue URI::InvalidURIError + link + end + + def path_or_url_helper(method, only_path, *args) + suffix = only_path ? "path" : "url" + helper_method = [method, suffix].join("_").to_sym + self.send(helper_method, *args) + end + def share_link_for(obj, only_path: false) - if obj.nil? - I18n.t('media_object.empty_share_link') - elsif obj.permalink.present? - obj.permalink - else - case obj - when MediaObjectBehavior - if only_path - media_object_path(obj) - else - media_object_url(obj) - end - when MasterFileBehavior - if only_path - id_section_media_object_path(obj.media_object_id, obj.id) - else - id_section_media_object_url(obj.media_object_id, obj.id) + return I18n.t('media_object.empty_share_link') if obj.nil? + + https_link do + if obj.permalink.present? + obj.permalink + else + case obj + when MediaObjectBehavior + if only_path + media_object_path(obj) + else + media_object_url(obj) + end + when MasterFileBehavior + if only_path + id_section_media_object_path(obj.media_object_id, obj.id) + else + id_section_media_object_url(obj.media_object_id, obj.id) + end end end end @@ -55,7 +76,7 @@ def lti_share_url_for(obj, _opts = {}) when Playlist then obj.to_gid_param when Timeline then obj.to_gid_param end - user_omniauth_callback_lti_url(target_id: target) + user_lti_omniauth_callback_url(target_id: target) end def image_for(document) @@ -214,8 +235,10 @@ def build_solr_request_from_response end def vgroup_display value - c = Course.find_by_context_id(value) - c.nil? ? value : (c.title || c.label || value) + Rails.cache.fetch("VGROUP_#{value}") do + c = Course.find_by_context_id(value) + c.nil? ? value : (c.title || c.label || value) + end end def truncate_center label, output_label_length, end_length = 0 diff --git a/app/helpers/blacklight/local_blacklight_helper.rb b/app/helpers/blacklight/local_blacklight_helper.rb index c11b38074e..c722d67bcf 100644 --- a/app/helpers/blacklight/local_blacklight_helper.rb +++ b/app/helpers/blacklight/local_blacklight_helper.rb @@ -25,6 +25,19 @@ def facet_group_names blacklight_config.facet_fields.map {|facet,opts| opts[:group]}.uniq end + def hide_course_from_user?(value) + current_user.nil? || !current_user.canvas_courses.keys.include?(value) + end + + def is_old_context_id?(value) + value.match?(/^[0-9a-z]{32,40}$/) + end + + def render_facet_item(facet_field, item) + return nil if facet_field == 'read_access_virtual_group_ssim' && hide_course_from_user?(item.value) + super + end + def url_for_document doc, options = {} SpeedyAF::Base.for(doc.to_h.with_indifferent_access) end diff --git a/app/helpers/google_tag_manager_helper.rb b/app/helpers/google_tag_manager_helper.rb new file mode 100644 index 0000000000..0ee7b441ce --- /dev/null +++ b/app/helpers/google_tag_manager_helper.rb @@ -0,0 +1,28 @@ +module GoogleTagManagerHelper + def render_google_tag_manager_head + return '' unless Settings.analytics_container_id.present? + + <<-HTML.strip_heredoc.html_safe + + + + HTML + end + + def render_google_tag_manager_body + return '' unless Settings.analytics_container_id.present? + + <<-HTML.strip_heredoc.html_safe + + + + HTML + end +end \ No newline at end of file diff --git a/app/helpers/upload_form_helper.rb b/app/helpers/upload_form_helper.rb index 153dc14971..bc926b9008 100644 --- a/app/helpers/upload_form_helper.rb +++ b/app/helpers/upload_form_helper.rb @@ -14,7 +14,7 @@ module UploadFormHelper def direct_upload? - Settings.encoding.engine_adapter.to_sym == :elastic_transcoder || Settings.minio.present? + [:elastic_transcoder, :media_convert].include?(Settings.encoding.engine_adapter.to_sym) || Settings.minio.present? end def upload_form_classes diff --git a/app/javascript/components/MediaObjectRamp.jsx b/app/javascript/components/MediaObjectRamp.jsx index 250e4d3ef1..172d7041e6 100644 --- a/app/javascript/components/MediaObjectRamp.jsx +++ b/app/javascript/components/MediaObjectRamp.jsx @@ -155,7 +155,7 @@ const Ramp = ({ : ( {sections_count > 0 && - +
{
}
diff --git a/app/javascript/components/PlaylistRamp.jsx b/app/javascript/components/PlaylistRamp.jsx index fb8e5927e9..fdf7eaf416 100644 --- a/app/javascript/components/PlaylistRamp.jsx +++ b/app/javascript/components/PlaylistRamp.jsx @@ -160,7 +160,7 @@ const Ramp = ({ startCanvasId={startCanvasId}> - + {playlist_item_ids?.length > 0 && ( @@ -194,13 +194,18 @@ const Ramp = ({ )} - + - {share.canShare && + {share.canShare && ( - } + )} diff --git a/app/javascript/components/ReactButtonContainer.jsx b/app/javascript/components/ReactButtonContainer.jsx index dc592f02f0..520d37b3ba 100644 --- a/app/javascript/components/ReactButtonContainer.jsx +++ b/app/javascript/components/ReactButtonContainer.jsx @@ -88,7 +88,7 @@ class ReactButtonContainer extends Component { Edit Structure - +
diff --git a/app/jobs/create_adaptive_playlist_job.rb b/app/jobs/create_adaptive_playlist_job.rb new file mode 100644 index 0000000000..2fce13fb1f --- /dev/null +++ b/app/jobs/create_adaptive_playlist_job.rb @@ -0,0 +1,65 @@ +require 'aws-sdk-s3' + +class CreateAdaptivePlaylistJob < ActiveJob::Base + queue_as :create_adaptive_playlist + + def perform(master_file_id) + master_file = MasterFile.find(master_file_id) + return false if master_file.derivatives.count == 0 + + playlist = adaptive_playlist(master_file) + return false if playlist.nil? + + write_playlist(master_file, playlist) + update_derivatives(master_file) + end + + private + + def key_for(master_file) + Addressable::URI.new(scheme: 's3', host: Settings.encoding.derivative_bucket, path: master_file.id + '/') + end + + def adaptive_playlist(master_file) + key = key_for(master_file) + streams = master_file.hls_streams.select { |hls| hls[:url].start_with?(key) } + return nil if streams.find { |hls| hls[:quality] == 'auto' } + + StringIO.new.tap do |result| + result.puts('#EXTM3U') + streams.each do |hls| + result.puts("#EXT-X-STREAM-INF:BANDWIDTH=#{hls[:bitrate]}") + result.puts(hls[:url].split(master_file.id).last.sub(/^\/+/, '')) + end + end.string + end + + def basename(master_file) + original_file = master_file.file_location + File.basename(original_file, File.extname(original_file)) + '.m3u8' + end + + def write_playlist(master_file, playlist) + digest = Digest::MD5.new << playlist + bucket = Aws::S3::Bucket.new(name: Settings.encoding.derivative_bucket) + key = File.join(master_file.id, basename(master_file)) + bucket.put_object( + key: key, + acl: 'private', + content_md5: digest.base64digest, + content_type: 'application/x-mpegURL', + body: playlist + ) + end + + def update_derivatives(master_file) + url = key_for(master_file).join(master_file.id).join(basename(master_file)).to_s + + derivative = master_file.derivatives.select { |d| d.quality == 'auto' }.first + derivative ||= Derivative.create(master_file: master_file, quality: 'auto') + derivative.audio_codec = master_file.derivatives.first.audio_codec + derivative.video_codec = master_file.derivatives.first.video_codec + derivative.location_url = derivative.hls_url = derivative.derivativeFile = url + derivative.save + end +end diff --git a/app/jobs/waveform_job.rb b/app/jobs/waveform_job.rb index 3ea2498f0d..0d3bfb8b88 100644 --- a/app/jobs/waveform_job.rb +++ b/app/jobs/waveform_job.rb @@ -48,12 +48,16 @@ def file_uri(master_file) end end - def derivative_file_uri(master_file) + def derivative_file_uri(master_file) derivatives = master_file.derivatives - # Find the lowest quality stream + # Find the lowest quality non-playlist stream ['low', 'medium', 'high'].each do |quality| - d = derivatives.select { |derivative| derivative.quality == quality }.first + d = derivatives.select do |derivative| + (derivative.quality == quality) && + ! derivative.absolute_location.match?(/\.(m3u8?|mpd|pls|asx|cue)$/) + end.first + if d.present? loc = FileLocator.new(d.absolute_location) return loc.uri if loc.exist? diff --git a/app/models/ability.rb b/app/models/ability.rb index 1ee62162a6..c166de3d4f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -245,6 +245,8 @@ def timeline_permissions can :read, Timeline do |timeline| timeline.valid_token?(@options[:timeline_token]) end + + cannot :create, Timeline # Disable Timeline UI end def checkout_permissions diff --git a/app/models/concerns/master_file_behavior.rb b/app/models/concerns/master_file_behavior.rb index d878514d6c..47cb78513f 100644 --- a/app/models/concerns/master_file_behavior.rb +++ b/app/models/concerns/master_file_behavior.rb @@ -92,7 +92,7 @@ def hls_streams format: d.format } hls << common.merge(url: d.streaming_url(true)) end - if hls.length > 1 + if hls.length > 1 && hls.find { |d| d[:quality] == 'auto' }.nil? hls << { quality: 'auto', mimetype: hls.first[:mimetype], format: hls.first[:format], diff --git a/app/models/course.rb b/app/models/course.rb index 2a4a1ffa1a..2f9f9b1039 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -16,7 +16,7 @@ class Course < ActiveRecord::Base # attr_accessible :context_id, :label, :title def self.autocomplete(query, _id = nil) - self.where("label LIKE :q OR title LIKE :q", q: "%#{query}%").collect { |course| + self.where("context_id LIKE :q OR title LIKE :q", q: "%#{query}%").collect { |course| { id: course.context_id, display: course.title } } end diff --git a/app/models/derivative.rb b/app/models/derivative.rb index a39d22b5b7..b8d71f52b4 100644 --- a/app/models/derivative.rb +++ b/app/models/derivative.rb @@ -119,7 +119,7 @@ def self.from_output(output, managed = true) # FIXME: Transform to stream url here? How do we distribute to the streaming server? derivative.location_url = output[:url] # For Intercom push - derivative.hls_url = output[:hls_url] if output[:hls_url].present? + derivative.hls_url = output[:hls_url] || output[:url] derivative.absolute_location = output[:url] diff --git a/app/models/master_file.rb b/app/models/master_file.rb index 789471d64c..19eb18f9d7 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -329,7 +329,12 @@ def update_progress_on_success!(encode) height: output.height } end - update_derivatives(outputs) + + managed = case Settings.encoding.manage_derivatives + when /^t(rue)?$/i, true, 1 then true + when /^f(alse)?$/i, false, 0 then false + end + update_derivatives(outputs, managed) run_hook :after_transcoding end @@ -573,17 +578,30 @@ def find_frame_source(options={}) source = FileLocator.new(working_file_path&.first || file_location) options[:non_temp_file] = true + options[:master] = true if source.source.blank? or (source.uri.scheme == 's3' and not source.exist?) source = FileLocator.new(self.derivatives.where(quality_ssi: 'high').first.absolute_location) options[:non_temp_file] = true end response = { source: source&.location }.merge(options) - return response if response[:source].to_s =~ %r(^https?://) + response_uri = URI(response[:source]) + return response if source.exist? && response_uri.scheme.match?(/^https?$/) && !response_uri.path.end_with?('.m3u8') unless File.exist?(response[:source]) Rails.logger.warn("Masterfile `#{file_location}` not found. Extracting via HLS.") - hls_temp_file, new_offset = create_frame_source_hls_temp_file(options[:offset]) - response = { source: hls_temp_file, offset: new_offset, non_temp_file: false } + begin + playlist_url = self.stream_details[:stream_hls].find { |d| d[:quality] == 'high' }[:url] + secure_url = SecurityHandler.secure_url(playlist_url, target: self.id) + playlist = Avalon::M3U8Reader.read(secure_url) + details = playlist.at(options[:offset]) + details[:location] = SecurityHandler.secure_url(Addressable::URI.unescape(details[:location])) + + # Fixes https://github.com/avalonmediasystem/avalon/issues/3474 + target_location = File.basename(details[:location]).split('?')[0] + target = File.join(Dir.tmpdir, target_location) + File.open(target,'wb') { |f| URI.open(details[:location]) { |io| f.write(io.read) } } + response = { source: target, offset: details[:offset], master: false } + end end return response end diff --git a/app/models/media_convert_encode.rb b/app/models/media_convert_encode.rb new file mode 100644 index 0000000000..6131a9bb3d --- /dev/null +++ b/app/models/media_convert_encode.rb @@ -0,0 +1,56 @@ +require 'aws-sdk-lambda' + +class MediaConvertEncode < WatchedEncode + self.engine_adapter = :media_convert + self.engine_adapter.queue = Settings&.encoding&.media_convert&.queue + self.engine_adapter.role = Settings&.encoding&.media_convert&.role + self.engine_adapter.output_bucket = Settings&.encoding&.derivative_bucket + + before_create prepend: true do |encode| + if encode.input.url =~ %r{s3://(.+?)/(.+)\.aiff$} + wav_url = "s3://#{$1}/wav_temp/#{$2}.wav" + Honeybadger.notify( + "AIFF file found. Invoking aiff2wav.", + class_name: "MediaConvertEncode", + tags: "encoding", + context: { + input_url: encode.input.url, + master_file_id: encode.options[:master_file_id] + } + ) + Rails.logger.warn("#{encode.input.url} is AIFF. Creating intermediate WAV file #{wav_url}.") + lambda_client = Aws::Lambda::Client.new + payload = { source: encode.input.url, dest: wav_url } + lambda_client.invoke(function_name: Settings.encoding.aiff_lambda, payload: payload.to_json) + encode.input.url = wav_url + end + + encode.options.merge!({ + masterfile_bucket: Settings.encoding.masterfile_bucket, + output_prefix: "#{options[:master_file_id]}/", + use_original_url: true + }).merge!(Settings.encoding.media_convert.configuration[:options][encode.options[:preset]]) + end + + reset_callbacks(:completed) + after_completed prepend: true do |encode| + map_outputs!(encode) + record = ActiveEncode::EncodeRecord.find_by(global_id: encode.to_global_id.to_s) + master_file = MasterFile.find(record.master_file_id) + if encode.input.url =~ %r{/wav_temp/} + Rails.logger.warn("Deleting intermediate WAV file #{encode.input.url}") + FileLocator::S3File.new(encode.input.url).object.delete + end + master_file.update_progress_on_success!(encode) + end + + def map_outputs!(encode) + Rails.logger.info("Mapping outputs for #{encode.to_global_id.to_s}") + mapping = Settings.encoding.media_convert.configuration[:mapping] + encode.output.collect! do |output| + suffix = output.id.split('-').last + output.label = mapping[suffix] || suffix + output + end + end +end diff --git a/app/models/mods_templates.rb b/app/models/mods_templates.rb index b376e9be87..40820aae04 100644 --- a/app/models/mods_templates.rb +++ b/app/models/mods_templates.rb @@ -138,7 +138,7 @@ def add_language(value, opts={}) define_template :_terms_of_use do |xml, text| xml.accessCondition(:type => 'use and reproduction'){ - xml.text(text) + xml.text(Array(text).join('; ')) } end diff --git a/app/models/redirect.rb b/app/models/redirect.rb new file mode 100644 index 0000000000..3fe3abecc4 --- /dev/null +++ b/app/models/redirect.rb @@ -0,0 +1,2 @@ +class Redirect < ApplicationRecord +end diff --git a/app/models/stream_token.rb b/app/models/stream_token.rb index 1837725f6d..219a678366 100644 --- a/app/models/stream_token.rb +++ b/app/models/stream_token.rb @@ -101,6 +101,6 @@ def self.valid_token?(value, master_file_id) end def renew! - update_attribute :expires, (Time.now.utc + Settings.streaming.stream_token_ttl.minutes) + update_attribute :expires, (Time.now.utc + Settings.streaming.stream_token_ttl.to_f.minutes) end end diff --git a/app/models/user.rb b/app/models/user.rb index be8fbef660..48821a1c03 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -86,6 +86,12 @@ def self.find_and_verify_by_email(email) user end + def self.find_by_devise_authentication_keys(values) + conditions = Devise.authentication_keys.map { |key| "lower(#{key}) IN (:value)" }.join(' OR ') + values = Array(values).map { |v| v.strip.downcase } + User.where("deleted_at IS NULL AND (#{conditions})", { value: values }).reject + end + def self.find_by_username_or_email(login) find_and_verify_by_username(login) || find_and_verify_by_email(login) end @@ -142,11 +148,23 @@ def in?(*list) list.flatten.include? user_key end + def canvas_courses + @canvas_courses ||= CanvasService.courses_for_user(username) + end + #TODO extract the ldap stuff into a mixin? def ldap_groups User.walk_ldap_groups(User.ldap_member_of(user_key), []).sort end + def user_key + email + end + + def virtual_groups + canvas_courses.respond_to?(:keys) ? ldap_groups + canvas_courses.keys : ldap_groups + end + def self.ldap_member_of(cn) return [] unless defined? Avalon::GROUP_LDAP entry = Avalon::GROUP_LDAP.search(:base => Avalon::GROUP_LDAP_TREE, :filter => Net::LDAP::Filter.eq("cn", cn), :attributes => ["memberof"])&.first diff --git a/app/services/canvas_service.rb b/app/services/canvas_service.rb new file mode 100644 index 0000000000..55880f45bd --- /dev/null +++ b/app/services/canvas_service.rb @@ -0,0 +1,48 @@ +class CanvasService + class << self + def client + if @client.nil? && Settings.canvas&.api&.token + @client = Faraday.new(Settings.canvas.api.endpoint) + @client.headers['Authorization'] = "Bearer #{Settings.canvas.api.token}" + end + @client + end + + def paged_results(path, params) + page = 0 + [].tap do |result| + while true + response = JSON.parse(client.get(path, params.merge(page: page+=1)).body) + break if response.empty? + result.concat(response) + end + end + end + + def find_course(code) + return nil if client.nil? + Rails.cache.fetch("CANVAS_COURSE_#{code}") do + paged_results('api/v1/accounts/self/courses', search_term: code).find do |found_course| + found_course['course_code'] == code + end + end + end + + def find_user(net_id) + return nil if client.nil? + result = paged_results('api/v1/accounts/self/users', search_term: net_id) + record = result.find { |entry| entry['login_id'] == net_id } + record&.fetch('id') + end + + def courses_for_user(net_id) + now = Time.now.utc + user = find_user(net_id) + return [] if user.nil? + result = paged_results("api/v1/users/#{user}/courses", enrollment_state: 'active').select do |course| + course['end_at'].nil? || Time.parse(course['end_at']) >= now + end + Hash[result.collect { |course| course.values_at('course_code', 'name') }] + end + end +end diff --git a/app/services/security_service.rb b/app/services/security_service.rb index 2ae377fb75..dc4f1067dc 100644 --- a/app/services/security_service.rb +++ b/app/services/security_service.rb @@ -17,15 +17,19 @@ class SecurityService def rewrite_url(url, context) case Settings.streaming.server.to_sym when :aws - configure_signer context[:protocol] ||= :stream_hls uri = Addressable::URI.parse(url) + expiration = Settings.streaming.stream_token_ttl.to_f.minutes.from_now case context[:protocol] when :stream_hls - Addressable::URI.join(Settings.streaming.http_base,uri.path).to_s + streaming_url = URI::DEFAULT_PARSER.escape(Addressable::URI.join(Settings.streaming.http_base,uri.path).to_s) + url_signer.signed_url(streaming_url, expires: expiration) else url end + when :s3 + uri = Addressable::URI.parse(url) + URI.join(Aws::S3::Bucket.new(name: uri.host).url, uri.path).to_s else session = context[:session] || { media_token: nil } token = context[:token] || StreamToken.find_or_create_session_token(session, context[:target]) @@ -37,15 +41,21 @@ def create_cookies(context) result = {} case Settings.streaming.server.to_sym when :aws - configure_signer domain = Addressable::URI.parse(Settings.streaming.http_base).host - cookie_domain = (context[:request_host].split(/\./) & domain.split(/\./)).join('.') + domain_segments = domain.split(/\./).reverse + stream_segments = context[:request_host].split(/\./).reverse + cookie_domain_segments = [] + domain_segments.each.with_index do |segment, index| + break if stream_segments[index] != segment + cookie_domain_segments << segment + end + cookie_domain = cookie_domain_segments.reverse.join('.') resource = "http*://#{domain}/#{context[:target]}/*" Rails.logger.info "Creating signed policy for resource #{resource}" - expiration = Settings.streaming.stream_token_ttl.minutes.from_now - params = Aws::CF::Signer.signed_params(resource, expires: expiration, resource: resource) - params.each_pair do |param,value| - result["CloudFront-#{param}"] = { + expiration = Settings.streaming.stream_token_ttl.to_f.minutes.from_now + policy = { Statement: [ { Resource: resource, Condition: { DateLessThan: { "AWS:EpochTime": expiration.to_i } } } ] }.to_json + cookie_signer.signed_cookie(resource, expires: expiration, policy: policy).each_pair do |key, value| + result[key] = { value: value, path: "/#{context[:target]}", domain: cookie_domain, @@ -57,25 +67,19 @@ def create_cookies(context) end private - - def configure_signer - require 'cloudfront-signer' - unless Aws::CF::Signer.is_configured? - Aws::CF::Signer.configure do |config| - key = case Settings.streaming.signing_key - when %r(^-----BEGIN) - Settings.streaming.signing_key - when %r(^s3://) - FileLocator::S3File.new(Settings.streaming.signing_key).object.get.body.read - when nil - Rails.logger.warn('No CloudFront signing key configured') - else - File.read(Settings.streaming.signing_key) - end - config.key = key - config.key_pair_id = Settings.streaming.signing_key_id - end + def cookie_signer + if @cookie_signer.nil? + require 'aws-sdk-cloudfront' + @cookie_signer = Aws::CloudFront::CookieSigner.new(key_pair_id: Settings.streaming.signing_key_id, private_key: Settings.streaming.signing_key) end + @cookie_signer end + def url_signer + if @url_signer.nil? + require 'aws-sdk-cloudfront' + @url_signer = Aws::CloudFront::UrlSigner.new(key_pair_id: Settings.streaming.signing_key_id, private_key: Settings.streaming.signing_key) + end + @url_signer + end end diff --git a/app/services/waveform_service.rb b/app/services/waveform_service.rb index fc0ce6f93b..f10e3b28b9 100644 --- a/app/services/waveform_service.rb +++ b/app/services/waveform_service.rb @@ -29,17 +29,13 @@ def get_waveform_json(uri) bits: @bit_res ) - peaks = if uri.scheme == 's3' - begin - local_file = FileLocator::S3File.new(uri).local_file - get_normalized_peaks(local_file.path) - ensure - local_file.close! - end - else - get_normalized_peaks(uri) - end + uri = Addressable::URI.parse(uri) + if uri.scheme == 's3' + s3_file = FileLocator::S3File.new(uri) + uri = Addressable::URI.parse(s3_file.object.presigned_url(:get)) + end + peaks = get_normalized_peaks(uri) peaks.each { |peak| waveform.append(peak[0], peak[1]) } return nil if waveform.size.zero? waveform.to_json diff --git a/app/views/layouts/avalon.html.erb b/app/views/layouts/avalon.html.erb index 3129b295b0..887ef50532 100644 --- a/app/views/layouts/avalon.html.erb +++ b/app/views/layouts/avalon.html.erb @@ -17,6 +17,7 @@ Unless required by applicable law or agreed to in writing, software distributed + <%= render_google_tag_manager_head %> <%= render_page_title %> @@ -34,10 +35,10 @@ Unless required by applicable law or agreed to in writing, software distributed <%= javascript_pack_tag 'application' %> <%= yield :page_styles %> <%= yield :additional_head_content %> - <%= render "modules/google_analytics" %> > + <%= render_google_tag_manager_body %>
Skip to main content diff --git a/app/views/layouts/embed.html.erb b/app/views/layouts/embed.html.erb index 7e4a9dff4a..45d6f4e84f 100644 --- a/app/views/layouts/embed.html.erb +++ b/app/views/layouts/embed.html.erb @@ -16,6 +16,7 @@ Unless required by applicable law or agreed to in writing, software distributed + <%= render_google_tag_manager_head %> <%= render_page_title %> @@ -27,10 +28,10 @@ Unless required by applicable law or agreed to in writing, software distributed <%= javascript_pack_tag 'embed' %> <%= yield :page_styles %> <%= yield :additional_head_content %> - <%= render "modules/google_analytics" %> + <%= render_google_tag_manager_body %> <%= yield %> <%= yield :page_scripts %> diff --git a/app/views/master_files/hls_manifest.m3u8.erb b/app/views/master_files/hls_manifest.m3u8.erb index 7b2db72b83..2dadfd12c2 100644 --- a/app/views/master_files/hls_manifest.m3u8.erb +++ b/app/views/master_files/hls_manifest.m3u8.erb @@ -16,5 +16,5 @@ Unless required by applicable law or agreed to in writing, software distributed #EXTM3U <% @hls_streams.each do |hls| %> #EXT-X-STREAM-INF:BANDWIDTH=<%= hls[:bitrate] %> -<%= hls[:url] %> -<% end %> \ No newline at end of file +<%= hls[:url].html_safe %> +<% end %> diff --git a/bin/boot_container b/bin/boot_container new file mode 100755 index 0000000000..316714e3af --- /dev/null +++ b/bin/boot_container @@ -0,0 +1,14 @@ +#!/bin/bash + +case "$CONTAINER_ROLE" in + "migrate") + bundle exec rake db:migrate zookeeper:upload zookeeper:create + ;; + "worker") + bundle exec rake shoryuken:create_config + exec bundle exec shoryuken --config=config/shoryuken.yml --no-daemon --rails + ;; + *) + exec bundle exec puma -C config/puma_container.rb + ;; +esac diff --git a/bin/run b/bin/run new file mode 100755 index 0000000000..a95b54fed7 --- /dev/null +++ b/bin/run @@ -0,0 +1,4 @@ +#!/bin/bash + +HOME=/home/app +sudo -u app -E -s $@ diff --git a/config/application.rb b/config/application.rb index 475af9a286..91ec72058a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -33,9 +33,27 @@ class Application < Rails::Application # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de - # config.eager_load_paths << Rails.root.join("extras") - config.active_job.queue_adapter = :sidekiq + if Settings&.active_job&.queue_adapter.present? + begin + require Settings.active_job.queue_adapter.to_s + rescue LoadError + end + config.active_job.queue_adapter = Settings.active_job.queue_adapter.to_s + else + config.active_job.queue_adapter = :sidekiq + end + + config.active_job.queue_name_prefix = Settings&.active_job&.queue_name_prefix + config.active_job.queue_name_delimiter = Settings&.active_job&.queue_name_delimiter || (config.active_job.queue_name_prefix.present? ? '-' : nil) + + # ActiveJob::Base gets configured with the queue prefix; ActionMailer::Base without + default_queue_name = [ + config.active_job.queue_name_prefix, + Settings&.active_job&.default_queue_name || 'default' + ].join(config.active_job.queue_name_delimiter) + ActionMailer::Base.deliver_later_queue_name = Settings&.active_job&.default_queue_name || 'default' + ActiveJob::Base.queue_name = default_queue_name config.action_dispatch.default_headers = { 'X-Frame-Options' => 'ALLOWALL' } @@ -59,6 +77,14 @@ class Application < Rails::Application config.middleware.insert_before 0, TempfileFactory + if Settings&.active_storage&.service_configurations.present? + configs = Settings.active_storage.service_configurations.to_hash + if config.active_storage.service_configurations.kind_of?(Hash) + config.active_storage.service_configurations.merge!(configs) + else + config.active_storage.service_configurations = configs + end + end config.active_storage.service = (Settings&.active_storage&.service.presence || "local").to_sym end end diff --git a/config/database.yml b/config/database.yml index d2e70a7f1f..f98e569169 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,27 +1,22 @@ -# SQLite version 3.x -# gem install sqlite3 -# -# Ensure the SQLite 3 gem is defined in your Gemfile -# gem 'sqlite3' -# default: &default - adapter: sqlite3 - pool: 10 - timeout: 10000 + adapter: postgresql + host: <%= ENV.fetch("DB_HOST") { "localhost" } %> + port: <%= ENV.fetch("DB_PORT") { 5432 } %> + encoding: unicode + pool: 5 + username: <%= ENV.fetch("DB_USER") { "docker" } %> + password: <%= ENV.fetch("DB_PASSWORD") { "d0ck3r" } %> development: <<: *default - database: db/development.sqlite3 + database: avr-development -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. test: <<: *default - pool: 20 - database: db/test.sqlite3 + database: avr-test production: - <<: *default - pool: 20 + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 database: db/production.sqlite3 diff --git a/config/environments/development.rb b/config/environments/development.rb index 067820227b..5b7e6d305e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -79,4 +79,11 @@ # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + config.web_console.permissions = '0.0.0.0/0' end diff --git a/config/environments/production.rb b/config/environments/production.rb index 1756dbaf7b..7b2d17ec70 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -56,6 +56,25 @@ # Include generic and useful information about system operation, but avoid logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Enable logging to both stdout and file, in more compact format + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + if ENV["RAILS_LOG_WITH_LOGRAGE"].present? + config.lograge.enabled = true + config.lograge.custom_options = -> (event) { { time: event.time } } + config.lograge.ignore_actions = ['CatalogController#index'] + config.lograge.formatter = Lograge::Formatters::Json.new + end + + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. config.log_level = :info # Suppress logger output for asset requests. @@ -101,4 +120,7 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false + + # Additional production specific initializers + Dir["config/environments/production/*.rb"].each {|file| load file } end diff --git a/config/initializers/_aws_config.rb b/config/initializers/_aws_config.rb new file mode 100644 index 0000000000..a2d253b140 --- /dev/null +++ b/config/initializers/_aws_config.rb @@ -0,0 +1,42 @@ +if ENV['SSM_PARAM_PATH'] + require 'aws-sdk-ssm' + + def add_param_to_hash(hash, param, path) + remove_segments = path.split(%r{/}).length + placement = param.name.split(%r{/})[remove_segments..-1] + placement.reject!(&:empty?) + key = placement.pop + target = hash + placement.each { |segment| target = (target[segment] ||= {}) } + target[key] = actual_value(param.value) + end + + def aws_param_hash(path: '/') + result = {} + ssm = Aws::SSM::Client.new + next_token = nil + loop do + response = ssm.get_parameters_by_path(path: path, recursive: true, with_decryption: true, next_token: next_token) + response.parameters.each do |param| + add_param_to_hash(result, param, path) + end + next_token = response.next_token + break if next_token.nil? + end + result + end + + def actual_value(v) + case v + when 'false' + false + when 'true' + true + else + Integer(v) rescue Float(v) rescue v + end + end + + Settings.add_source!(aws_param_hash(path: "#{ENV['SSM_PARAM_PATH']}/Settings")) + Settings.reload! +end \ No newline at end of file diff --git a/config/initializers/active_encode.rb b/config/initializers/active_encode.rb index 1a688a5466..6865d7ce46 100644 --- a/config/initializers/active_encode.rb +++ b/config/initializers/active_encode.rb @@ -6,6 +6,10 @@ ActiveEncode::EngineAdapters::FfmpegAdapter.completeness_threshold = 95 when :matterhorn Rubyhorn.init + when :mediaconvert + ActiveEncode::Base.engine_adapter.role = Settings.encoding.media_convert.role + ActiveEncode::Base.engine_adapter.output_bucket = Settings.encoding.derivative_bucket + ActiveEncode::Base.engine_adapter.setup! when :elastic_transcoder require 'aws-sdk-elastictranscoder' require 'avalon/elastic_transcoder_encode' diff --git a/config/initializers/active_fedora_env.rb b/config/initializers/active_fedora_env.rb index 0ddccbe616..32e00d05cb 100644 --- a/config/initializers/active_fedora_env.rb +++ b/config/initializers/active_fedora_env.rb @@ -11,11 +11,12 @@ def load_fedora_config if fedora_setting.present? ActiveFedora::Base.logger.info("ActiveFedora: loading fedora config from FEDORA_URL") if ActiveFedora::Base.logger fedora_url = URI.parse(fedora_setting) - @fedora_config = { user: fedora_url.user, password: fedora_url.password, base_path: ENV['FEDORA_BASE_PATH'] || "" } + @fedora_config = { user: fedora_url.user, password: fedora_url.password, base_path: ERB.new(Settings.fedora&.base_path || ENV['FEDORA_BASE_PATH'] || "").result } fedora_url.userinfo = '' @fedora_config[:url] = fedora_url.to_s @fedora_config[:request] = { timeout: Float(fedora_timeout), open_timeout: Float(fedora_timeout) } unless fedora_timeout.blank? - ENV['FEDORA_URL'] ||= fedora_setting + ENV['FEDORA_URL'] ||= @fedora_config[:url] + ENV['FEDORA_BASE_PATH'] ||= @fedora_config[:base_path] else super end @@ -29,8 +30,8 @@ def load_solr_config solr_setting = Settings.solr_url || ENV['SOLR_URL'] if solr_setting.present? ActiveFedora::Base.logger.info("ActiveFedora: loading solr config from SOLR_URL") if ActiveFedora::Base.logger - @solr_config = { url: solr_setting } - ENV['SOLR_URL'] ||= solr_setting + @solr_config = { url: ERB.new(solr_setting).result } + ENV['SOLR_URL'] ||= @solr_config[:url] else super end diff --git a/config/initializers/avalon_lti.rb b/config/initializers/avalon_lti.rb index 3dee05d751..54cf821d86 100644 --- a/config/initializers/avalon_lti.rb +++ b/config/initializers/avalon_lti.rb @@ -1,11 +1,13 @@ # You also need to explicitly enable OAuth 1 support in the environment.rb or an initializer: AUTH_10_SUPPORT = true +OAuth::Signature.available_methods.keys.each do |method| + OAuth::Signature.available_methods[method.upcase] = OAuth::Signature.available_methods[method] +end module Avalon module Lti begin - Configuration = - YAML.load(ERB.new(File.read(File.expand_path('../../lti.yml', __FILE__))).result) + Configuration = YAML.load(File.read(File.expand_path('../../lti.yml',__FILE__))) rescue Configuration = {} end diff --git a/config/initializers/aws.rb b/config/initializers/aws.rb index ff540a99fe..8fc0b6c010 100644 --- a/config/initializers/aws.rb +++ b/config/initializers/aws.rb @@ -2,12 +2,23 @@ require "aws-sdk-s3" Aws.config.update( + s3: { endpoint: Settings.minio.endpoint, access_key_id: Settings.minio.access, secret_access_key: Settings.minio.secret, region: ENV["AWS_REGION"] + } ) - # Service specific global configuration Aws.config[:s3] = { force_path_style: true } end + +if Settings.sqs + require "aws-sdk-sqs" + Aws.config.update( + sqs: { + endpoint: Settings.sqs ? Settings.sqs.endpoint : nil, + region: ENV["AWS_REGION"] + } + ) +end diff --git a/config/initializers/cache_store.rb b/config/initializers/cache_store.rb index 699940c3b0..80b5f34ac4 100644 --- a/config/initializers/cache_store.rb +++ b/config/initializers/cache_store.rb @@ -3,10 +3,18 @@ redis_host = Settings.redis.host redis_port = Settings.redis.port || 6379 redis_db = Settings.redis.db || 0 + +Redis.new(host: Settings.redis.host, port: Settings.redis.port).tap do |redis| + (setting, value) = redis.config("GET", "replica-read-only") + if value == "yes" + redis.config("SET", "replica-read-only", "no") + end +rescue Redis::CannotConnectError, Redis::CommandError + # Don't worry about it +end + config.cache_store = :redis_store, { host: redis_host, port: redis_port, - db: redis_db, - namespace: 'avalon' + db: redis_db } - diff --git a/config/initializers/config.rb b/config/initializers/config.rb index c51f6429f0..acea97f355 100644 --- a/config/initializers/config.rb +++ b/config/initializers/config.rb @@ -11,7 +11,7 @@ # config.overwrite_arrays = true # Load environment variables from the `ENV` object and override any settings defined in files. - config.use_env = true + config.use_env = true unless Rails.env.test? # Define ENV variable prefix deciding which variables to load into config. config.env_prefix = 'SETTINGS' diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index c94cefe824..4dec934450 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,354 +1,353 @@ -Rails.application.reloader.to_prepare do - # Use this hook to configure devise mailer, warden hooks and so forth. - # Many of these configuration options can be set straight in your model. - Devise.setup do |config| - # The secret key used by Devise. Devise uses this key to generate - # random tokens. Changing this key will render invalid all existing - # confirmation, reset password and unlock tokens in the database. - # Devise will use the `secret_key_base` as its `secret_key` - # by default. You can change it below and use your own secret key. - # config.secret_key = '954891a731718dd9c577d1be0d85c6f4bae4dbd43a82864954ad82523ba708317b28d3c5a7cde5288cddea43884a87c16d6501678fc92426599c6d9277c8aec5' - - # ==> Mailer Configuration - # Configure the e-mail address which will be shown in Devise::Mailer, - # note that it will be overwritten if you use your own mailer class - # with default "from" parameter. - config.mailer_sender = Settings.email.notification - - # Configure the class responsible to send e-mails. - # config.mailer = 'Devise::Mailer' - - # Configure the parent class responsible to send e-mails. - # config.parent_mailer = 'ActionMailer::Base' - - # ==> ORM configuration - # Load and configure the ORM. Supports :active_record (default) and - # :mongoid (bson_ext recommended) by default. Other ORMs may be - # available as additional gems. - require 'devise/orm/active_record' - - # ==> Configuration for any authentication mechanism - # Configure which keys are used when authenticating a user. The default is - # just :email. You can configure it to use [:login, :subdomain], so for - # authenticating a user, both parameters are required. Remember that those - # parameters are used only when authenticating and not when retrieving from - # session. If you need permissions, you should implement that in a before filter. - # You can also supply a hash where the value is a boolean determining whether - # or not authentication should be aborted when the value is not present. - config.authentication_keys = [:username] - - # Configure parameters from the request object used for authentication. Each entry - # given should be a request method and it will automatically be passed to the - # find_for_authentication method and considered in your model lookup. For instance, - # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. - # The same considerations mentioned for authentication_keys also apply to request_keys. - # config.request_keys = [] - - # Configure which authentication keys should be case-insensitive. - # These keys will be downcased upon creating or modifying a user and when used - # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [:username, :login, :email] - - # Configure which authentication keys should have whitespace stripped. - # These keys will have whitespace before and after removed upon creating or - # modifying a user and when used to authenticate or find a user. Default is :email. - config.strip_whitespace_keys = [:username, :login, :email] - - # Tell if authentication through request.params is enabled. True by default. - # It can be set to an array that will enable params authentication only for the - # given strategies, for example, `config.params_authenticatable = [:database]` will - # enable it only for database (email + password) authentication. - # config.params_authenticatable = true - - # Tell if authentication through HTTP Auth is enabled. False by default. - # It can be set to an array that will enable http authentication only for the - # given strategies, for example, `config.http_authenticatable = [:database]` will - # enable it only for database authentication. The supported strategies are: - # :database = Support basic authentication with authentication key + password - # config.http_authenticatable = false - - # If 401 status code should be returned for AJAX requests. True by default. - # config.http_authenticatable_on_xhr = true - - # The realm used in Http Basic Authentication. 'Application' by default. - # config.http_authentication_realm = 'Application' - - # It will change confirmation, password recovery and other workflows - # to behave the same regardless if the e-mail provided was right or wrong. - # Does not affect registerable. - # config.paranoid = true - - # By default Devise will store the user in session. You can skip storage for - # particular strategies by setting this option. - # Notice that if you are skipping storage for all authentication paths, you - # may want to disable generating routes to Devise's sessions controller by - # passing skip: :sessions to `devise_for` in your config/routes.rb - config.skip_session_storage = [:http_auth] - - # By default, Devise cleans up the CSRF token on authentication to - # avoid CSRF token fixation attacks. This means that, when using AJAX - # requests for sign in and sign up, you need to get a new CSRF token - # from the server. You can disable this option at your own risk. - # config.clean_up_csrf_token_on_authentication = true - - # When false, Devise will not attempt to reload routes on eager load. - # This can reduce the time taken to boot the app but if your application - # requires the Devise mappings to be loaded during boot time the application - # won't boot properly. - # config.reload_routes = true - - # ==> Configuration for :database_authenticatable - # For bcrypt, this is the cost for hashing the password and defaults to 11. If - # using other algorithms, it sets how many times you want the password to be hashed. - # - # Limiting the stretches to just one in testing will increase the performance of - # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use - # a value less than 10 in other environments. Note that, for bcrypt (the default - # algorithm), the cost increases exponentially with the number of stretches (e.g. - # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). - config.stretches = Rails.env.test? ? 1 : 11 - - # Set up a pepper to generate the hashed password. - # config.pepper = '2494a81cfee550c3640539b4bcbfa986f6559011bd05d896d6d6720d6d56a17bd5f3cdab2d975f94c3ae4fa39c07ddae1a07dddc28693caf33a29f01e3fac0d6' - - # Send a notification email when the user's password is changed - # config.send_password_change_notification = false - - # ==> Configuration for :invitable - # The period the generated invitation token is valid, after - # this period, the invited resource won't be able to accept the invitation. - # When invite_for is 0 (the default), the invitation won't expire. - # config.invite_for = 2.weeks - - # Number of invitations users can send. - # - If invitation_limit is nil, there is no limit for invitations, users can - # send unlimited invitations, invitation_limit column is not used. - # - If invitation_limit is 0, users can't send invitations by default. - # - If invitation_limit n > 0, users can send n invitations. - # You can change invitation_limit column for some users so they can send more - # or less invitations, even with global invitation_limit = 0 - # Default: nil - # config.invitation_limit = 5 - - # The key to be used to check existing users when sending an invitation - # and the regexp used to test it when validate_on_invite is not set. - # config.invite_key = {:email => /\A[^@]+@[^@]+\z/} - # config.invite_key = {:email => /\A[^@]+@[^@]+\z/, :login => nil} - - # Flag that force a record to be valid before being actually invited - # Default: false - # config.validate_on_invite = true - - # Resend invitation if user with invited status is invited again - # Default: true - # config.resend_invitation = false - - # The class name of the inviting model. If this is nil, - # the #invited_by association is declared to be polymorphic. - # Default: nil - # config.invited_by_class_name = 'User' - - # The foreign key to the inviting model (if invited_by_class_name is set) - # Default: :invited_by_id - # config.invited_by_foreign_key = :invited_by_id - - # The column name used for counter_cache column. If this is nil, - # the #invited_by association is declared without counter_cache. - # Default: nil - # config.invited_by_counter_cache = :invitations_count - - # Auto-login after the user accepts the invite. If this is false, - # the user will need to manually log in after accepting the invite. - # Default: true - # config.allow_insecure_sign_in_after_accept = false - - # ==> Configuration for :confirmable - # A period that the user is allowed to access the website even without - # confirming their account. For instance, if set to 2.days, the user will be - # able to access the website for two days without confirming their account, - # access will be blocked just in the third day. Default is 0.days, meaning - # the user cannot access the website without confirming their account. - # config.allow_unconfirmed_access_for = 2.days - - # A period that the user is allowed to confirm their account before their - # token becomes invalid. For example, if set to 3.days, the user can confirm - # their account within 3 days after the mail was sent, but on the fourth day - # their account can't be confirmed with the token any more. - # Default is nil, meaning there is no restriction on how long a user can take - # before confirming their account. - # config.confirm_within = 3.days - - # If true, requires any email changes to be confirmed (exactly the same way as - # initial account confirmation) to be applied. Requires additional unconfirmed_email - # db field (see migrations). Until confirmed, new email is stored in - # unconfirmed_email column, and copied to email column on successful confirmation. - config.reconfirmable = true - - # Defines which key will be used when confirming an account - # config.confirmation_keys = [:email] - - # ==> Configuration for :rememberable - # The time the user will be remembered without asking for credentials again. - # config.remember_for = 2.weeks - - # Invalidates all the remember me tokens when the user signs out. - config.expire_all_remember_me_on_sign_out = true - - # If true, extends the user's remember period when remembered via cookie. - # config.extend_remember_period = false - - # Options to be passed to the created cookie. For instance, you can set - # secure: true in order to force SSL only cookies. - # config.rememberable_options = {} - - # ==> Configuration for :validatable - # Range for password length. - config.password_length = 6..128 - - # Email regex used to validate email formats. It simply asserts that - # one (and only one) @ exists in the given string. This is mainly - # to give user feedback and not to assert the e-mail validity. - config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ - - # ==> Configuration for :timeoutable - # The time you want to timeout the user session without activity. After this - # time the user will be asked for credentials again. Default is 30 minutes. - # config.timeout_in = 30.minutes - - # ==> Configuration for :lockable - # Defines which strategy will be used to lock an account. - # :failed_attempts = Locks an account after a number of failed attempts to sign in. - # :none = No lock strategy. You should handle locking by yourself. - # config.lock_strategy = :failed_attempts - - # Defines which key will be used when locking and unlocking an account - # config.unlock_keys = [:email] - - # Defines which strategy will be used to unlock an account. - # :email = Sends an unlock link to the user email - # :time = Re-enables login after a certain amount of time (see :unlock_in below) - # :both = Enables both strategies - # :none = No unlock strategy. You should handle unlocking by yourself. - # config.unlock_strategy = :both - - # Number of authentication tries before locking an account if lock_strategy - # is failed attempts. - # config.maximum_attempts = 20 - - # Time interval to unlock the account if :time is enabled as unlock_strategy. - # config.unlock_in = 1.hour - - # Warn on the last attempt before the account is locked. - # config.last_attempt_warning = true - - # ==> Configuration for :recoverable - # - # Defines which key will be used when recovering the password for an account - # config.reset_password_keys = [:email] - - # Time interval you can reset your password with a reset password key. - # Don't put a too small interval or your users won't have the time to - # change their passwords. - config.reset_password_within = 6.hours - - # When set to false, does not sign a user in automatically after their password is - # reset. Defaults to true, so a user is signed in automatically after a reset. - # config.sign_in_after_reset_password = true - - # ==> Configuration for :encryptable - # Allow you to use another hashing or encryption algorithm besides bcrypt (default). - # You can use :sha1, :sha512 or algorithms from others authentication tools as - # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 - # for default behavior) and :restful_authentication_sha1 (then you should set - # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). - # - # Require the `devise-encryptable` gem when using anything other than bcrypt - # config.encryptor = :sha512 - - # ==> Scopes configuration - # Turn scoped views on. Before rendering "sessions/new", it will first check for - # "users/sessions/new". It's turned off by default because it's slower if you - # are using only default views. - # config.scoped_views = false - - # Configure the default scope given to Warden. By default it's the first - # devise role declared in your routes (usually :user). - # config.default_scope = :user - - # Set this configuration to false if you want /users/sign_out to sign out - # only the current scope. By default, Devise signs out all scopes. - # config.sign_out_all_scopes = true - - # ==> Navigation configuration - # Lists the formats that should be treated as navigational. Formats like - # :html, should redirect to the sign in page when the user does not have - # access, but formats like :xml or :json, should return 401. - # - # If you have any extra navigational formats, like :iphone or :mobile, you - # should add them to the navigational formats lists. - # - # The "*/*" below is required to match Internet Explorer requests. - # config.navigational_formats = ['*/*', :html] - - # The default HTTP method used to sign out a resource. Default is :delete. - config.sign_out_via = :get - - # ==> OmniAuth - # Add a new OmniAuth provider. Check the wiki for more information on setting - # up on your models and hooks. - # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' - Avalon::Authentication::Providers.each do |provider| - if provider[:provider] == :lti - provider[:params].merge!({consumers: Avalon::Lti::Configuration}) - end - if provider[:provider] == :identity - provider[:params].merge!({ - on_login: AuthFormsController.action(:render_identity_request_form), - on_registration: AuthFormsController.action(:render_identity_registration_form), - on_failed_registration: AuthFormsController.action(:render_form_with_errors) - }) - end - params = provider[:params] - params = [params] unless params.is_a?(Array) - begin - require "omniauth/#{provider[:provider]}" - rescue LoadError - require "omniauth-#{provider[:provider]}" - end - config.omniauth provider[:provider], *params +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '954891a731718dd9c577d1be0d85c6f4bae4dbd43a82864954ad82523ba708317b28d3c5a7cde5288cddea43884a87c16d6501678fc92426599c6d9277c8aec5' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = Settings.email.notification + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + +# ==> Configuration for any authentication mechanism +# Configure which keys are used when authenticating a user. The default is +# just :email. You can configure it to use [:login, :subdomain], so for +# authenticating a user, both parameters are required. Remember that those +# parameters are used only when authenticating and not when retrieving from +# session. If you need permissions, you should implement that in a before filter. +# You can also supply a hash where the value is a boolean determining whether +# or not authentication should be aborted when the value is not present. +config.authentication_keys = [:email, :username] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:username, :login, :email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:username, :login, :email] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 11. If + # using other algorithms, it sets how many times you want the password to be hashed. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 11 + + # Set up a pepper to generate the hashed password. + # config.pepper = '2494a81cfee550c3640539b4bcbfa986f6559011bd05d896d6d6720d6d56a17bd5f3cdab2d975f94c3ae4fa39c07ddae1a07dddc28693caf33a29f01e3fac0d6' + + # Send a notification email when the user's password is changed + # config.send_password_change_notification = false + + # ==> Configuration for :invitable + # The period the generated invitation token is valid, after + # this period, the invited resource won't be able to accept the invitation. + # When invite_for is 0 (the default), the invitation won't expire. + # config.invite_for = 2.weeks + + # Number of invitations users can send. + # - If invitation_limit is nil, there is no limit for invitations, users can + # send unlimited invitations, invitation_limit column is not used. + # - If invitation_limit is 0, users can't send invitations by default. + # - If invitation_limit n > 0, users can send n invitations. + # You can change invitation_limit column for some users so they can send more + # or less invitations, even with global invitation_limit = 0 + # Default: nil + # config.invitation_limit = 5 + + # The key to be used to check existing users when sending an invitation + # and the regexp used to test it when validate_on_invite is not set. + # config.invite_key = {:email => /\A[^@]+@[^@]+\z/} + # config.invite_key = {:email => /\A[^@]+@[^@]+\z/, :login => nil} + + # Flag that force a record to be valid before being actually invited + # Default: false + # config.validate_on_invite = true + + # Resend invitation if user with invited status is invited again + # Default: true + # config.resend_invitation = false + + # The class name of the inviting model. If this is nil, + # the #invited_by association is declared to be polymorphic. + # Default: nil + # config.invited_by_class_name = 'User' + + # The foreign key to the inviting model (if invited_by_class_name is set) + # Default: :invited_by_id + # config.invited_by_foreign_key = :invited_by_id + + # The column name used for counter_cache column. If this is nil, + # the #invited_by association is declared without counter_cache. + # Default: nil + # config.invited_by_counter_cache = :invitations_count + + # Auto-login after the user accepts the invite. If this is false, + # the user will need to manually log in after accepting the invite. + # Default: true + # config.allow_insecure_sign_in_after_accept = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. Default is 0.days, meaning + # the user cannot access the website without confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :get + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + Avalon::Authentication::Providers.each do |provider| + if provider[:provider] == :lti + provider[:params].merge!({consumers: Avalon::Lti::Configuration}) end - - # ==> Warden configuration - # If you want to use other strategies, that are not supported by Devise, or - # change the failure app, you can configure them inside the config.warden block. - # - # config.warden do |manager| - # manager.intercept_401 = false - # manager.default_strategies(scope: :user).unshift :some_external_strategy - # end - - # ==> Mountable engine configurations - # When using Devise inside an engine, let's call it `MyEngine`, and this engine - # is mountable, there are some extra configurations to be taken into account. - # The following options are available, assuming the engine is mounted as: - # - # mount MyEngine, at: '/my_engine' - # - # The router that invoked `devise_for`, in the example above, would be: - # config.router_name = :my_engine - # - # When using OmniAuth, Devise cannot automatically set OmniAuth path, - # so you need to do it manually. For the users scope, it would be: - # config.omniauth_path_prefix = '/my_engine/users/auth' - OmniAuth.config.logger = Rails.logger + if provider[:provider] == :identity + provider[:params].merge!({ + on_login: AuthFormsController.action(:render_identity_request_form), + on_registration: AuthFormsController.action(:render_identity_registration_form), + on_failed_registration: AuthFormsController.action(:render_form_with_errors) + }) + end + params = provider[:params] + params = [params] unless params.is_a?(Array) + begin + require "omniauth/#{provider[:provider]}" + rescue LoadError + require "omniauth-#{provider[:provider]}" + end + config.omniauth provider[:provider], *params end - # Override script_name to always return empty string and avoid looking in @env - # This override is needed due to our direct rendering of the identity login form in AuthFormsController - # which doesn't initialize @env leading to a NoMethodError when trying read a hash value from it. - OmniAuth::Strategies::Identity.class_eval do - def script_name - '' - end + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' + OmniAuth.config.logger = Rails.logger +end + +# Override script_name to always return empty string and avoid looking in @env +# This override is needed due to our direct rendering of the identity login form in AuthFormsController +# which doesn't initialize @env leading to a NoMethodError when trying read a hash value from it. +OmniAuth::Strategies::Identity.class_eval do + def script_name + '' end end diff --git a/config/initializers/logging_config.rb b/config/initializers/logging_config.rb new file mode 100644 index 0000000000..323f2d192f --- /dev/null +++ b/config/initializers/logging_config.rb @@ -0,0 +1 @@ +Rails.logger.level = Settings.log_level.to_sym unless Settings.log_level.nil? diff --git a/config/initializers/media_convert_config.rb b/config/initializers/media_convert_config.rb new file mode 100644 index 0000000000..0eab17714d --- /dev/null +++ b/config/initializers/media_convert_config.rb @@ -0,0 +1,20 @@ +Settings.encoding.media_convert ||= Config::Options.new +Settings.encoding.media_convert.configuration = { + mapping: {'720' => 'high', '540' => 'low'}, + options: { + 'avalon' => { + media_type: :video, + outputs: [ + {preset: "avr-video-medium", modifier: "-720"}, + {preset: "avr-video-low", modifier: "-540"} + ] + }, + 'fullaudio' => { + media_type: :audio, + outputs: [ + {preset: "avr-audio-high", modifier: "-high"}, + {preset: "avr-audio-medium", modifier: "-medium"} + ] + } + } +} \ No newline at end of file diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb new file mode 100644 index 0000000000..94a441b049 --- /dev/null +++ b/config/initializers/omniauth.rb @@ -0,0 +1,4 @@ +OmniAuth.configure do |config| + config.allowed_request_methods << :get + config.silence_get_warning = true +end \ No newline at end of file diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index f669ba1e7e..f3e0da32f7 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -13,15 +13,14 @@ Sidekiq::Web.disable(:sessions) require 'sidekiq/cron/web' -Rails.application.config.to_prepare do - begin - # Only create cron jobs if Sidekiq can connect to Redis - Sidekiq.redis(&:info) - Sidekiq::Cron::Job.create(name: 'Scan for batches - every 1min', cron: '*/1 * * * *', class: 'BatchScanJob') - Sidekiq::Cron::Job.create(name: 'Status Checking and Email Notification of Existing Batches - every 15min', cron: '*/15 * * * *', class: 'IngestBatchStatusEmailJobs::IngestFinished') - Sidekiq::Cron::Job.create(name: 'Status Checking and Email Notification for Stalled Batches - every 1day', cron: '0 1 * * *', class: 'IngestBatchStatusEmailJobs::StalledJob') - Sidekiq::Cron::Job.create(name: 'Clean out user sessions older than 7 days - every 6hour', cron: '0 */6 * * *', class: 'CleanupSessionJob') - rescue Redis::CannotConnectError => e - Rails.logger.warn "Cannot create sidekiq-cron jobs: #{e.message}" - end -end + +# begin +# # Only create cron jobs if Sidekiq can connect to Redis +# Sidekiq.redis(&:info) +# Sidekiq::Cron::Job.create(name: 'Scan for batches - every 1min', cron: '*/1 * * * *', class: 'BatchScanJob') +# Sidekiq::Cron::Job.create(name: 'Status Checking and Email Notification of Existing Batches - every 15min', cron: '*/15 * * * *', class: 'IngestBatchStatusEmailJobs::IngestFinished') +# Sidekiq::Cron::Job.create(name: 'Status Checking and Email Notification for Stalled Batches - every 1day', cron: '0 1 * * *', class: 'IngestBatchStatusEmailJobs::StalledJob') +# Sidekiq::Cron::Job.create(name: 'Clean out user sessions older than 7 days - every 6hour', cron: '0 */6 * * *', class: 'CleanupSessionJob') +# rescue Redis::CannotConnectError => e +# Rails.logger.warn "Cannot create sidekiq-cron jobs: #{e.message}" +# end diff --git a/config/lti.yml b/config/lti.yml new file mode 100644 index 0000000000..85151147be --- /dev/null +++ b/config/lti.yml @@ -0,0 +1,16 @@ +--- +e3285bfcd6cb4332a84aa14da6a3eaad: + :uid: :user_id + :email: :lis_person_contact_email_primary + :context_id: :context_id + :context_name: :context_title +7db438071375c02373713c12c73869ff2f470b68.new-northwestern.instructure.com: + :uid: :lis_person_sourcedid + :email: :lis_person_contact_email_primary + :context_id: :context_label + :context_name: :context_title +avalon.ares.atlas-sys.com: + :uid: :lis_person_sourcedid + :email: :lis_person_contact_email_primary + :context_id: :context_id + :context_name: :context_title diff --git a/config/nu_vocab.yml b/config/nu_vocab.yml new file mode 100644 index 0000000000..ab6806f3d7 --- /dev/null +++ b/config/nu_vocab.yml @@ -0,0 +1,44 @@ +units: + - Charles Deering McCormick Library of Special Collections + - Center for Scholarly Communication and Digital Curation + - Galter Health Sciences Library, Special Collections + - Melville J. Herskovits Library of African Studies + - Music Library + - Northwestern University Archives + - Northwestern University Libraries + - Northwestern University Pritzker Legal Research Center + - Northwestern University Transportation Library + - Marjorie I. Mitchell Multimedia Center + - WCAS Multimedia Learning Center +identifier_types: + local: Catalog Key + oclc: OCLC + lccn: LCCN + issue number: Issue Number + matrix number: Matrix Number + music publisher: Music Publisher/Label + videorecording identifier: Videorecording Identifier + other: Other +note_types: + general: General Note + awards: Awards + biographical/historical: Biographical/Historical Note + creation/production credits: Creation/Production Credits + language: Language Note + local: Local Note + performers: Performers + statement of responsibility: Statement of Responsibility + venue: Venue/Event Date +rights_statements: + http://rightsstatements.org/vocab/InC/1.0/: In Copyright + http://rightsstatements.org/vocab/InC-OW-EU/1.0/: In Copyright - EU Orphan Work + http://rightsstatements.org/vocab/InC-EDU/1.0/: In Copyright - Educational Use Permitted + http://rightsstatements.org/vocab/InC-NC/1.0/: In Copyright - Non-Commercial Use Permitted + http://rightsstatements.org/vocab/InC-RUU/1.0/: In Copyright - Rights-Holders Unlocatable or Unidentifiable + http://rightsstatements.org/vocab/NoC-CR/1.0/: No Copyright - Contractual Restrictions + http://rightsstatements.org/vocab/NoC-NC/1.0/: No Copyright - Non-Commercial Use Only + http://rightsstatements.org/vocab/NoC-OKLR/1.0/: No Copyright - Other Known Legal Restrictions + http://rightsstatements.org/vocab/NoC-US/1.0/: No Copyright - United States + http://rightsstatements.org/vocab/CNE/1.0/: Copyright Not Evaluated + http://rightsstatements.org/vocab/UND/1.0/: Copyright Undetermined + http://rightsstatements.org/vocab/NKC/1.0/: No Known Copyright diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000000..2f32227c90 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,4 @@ +bind 'tcp://0.0.0.0:3000' +ssl_bind '0.0.0.0', '3001', + cert: ENV['SSL_CERT'], + key: ENV['SSL_KEY'] \ No newline at end of file diff --git a/config/puma_container.rb b/config/puma_container.rb new file mode 100644 index 0000000000..4ba0da7950 --- /dev/null +++ b/config/puma_container.rb @@ -0,0 +1,4 @@ +threads 8, 32 +workers `grep -c processor /proc/cpuinfo` +port ENV.fetch('PORT') { 3000 } +pidfile '/var/run/puma/puma.pid' diff --git a/config/resque.yml b/config/resque.yml index 7ce20cff98..ea6c7dc47f 100644 --- a/config/resque.yml +++ b/config/resque.yml @@ -1,2 +1,2 @@ -development: localhost:6379 -test: localhost:6379 +development: localhost:<%= ENV['REDIS_DEV_PORT'] || 6379 %> +test: localhost:<%= ENV['REDIS_TEST_PORT'] || 6379 %> diff --git a/config/routes.rb b/config/routes.rb index d9d6e7c836..ee843671ca 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -62,9 +62,6 @@ devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks', sessions: 'users/sessions' } devise_scope :user do match '/users/auth/:provider', to: 'users/omniauth_callbacks#passthru', as: :user_omniauth_authorize, via: [:get, :post] - Avalon::Authentication::Providers.collect { |provider| provider[:provider] }.uniq.each do |provider_name| - match "/users/auth/#{provider_name}/callback", to: "users/omniauth_callbacks##{provider_name}", as: "user_omniauth_callback_#{provider_name}".to_sym, via: [:get, :post] - end end mount BrowseEverything::Engine => '/browse' diff --git a/config/settings.yml b/config/settings.yml index 5b5b039022..5d576b75f6 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -51,7 +51,7 @@ app_job: solr_and_fedora: raise_on_connection_error: true zookeeper: - connection_str: "localhost:9983/configs" + connection_str: 'localhost:9983/configs' streaming: server: :generic stream_token_ttl: 20 #minutes @@ -91,7 +91,6 @@ auth: # :params: # :oauth_credentials: # somekey: somesecret -# google_analytics_tracking_id: "someid" supplemental_files: proxy: false waveform: diff --git a/config/settings/production.yml b/config/settings/production.yml index e69de29bb2..53eda17b81 100644 --- a/config/settings/production.yml +++ b/config/settings/production.yml @@ -0,0 +1,2 @@ +controlled_vocabulary: + path: config/nu_vocab.yml \ No newline at end of file diff --git a/config/settings/test.yml b/config/settings/test.yml index 329396e22c..64a79ef26a 100644 --- a/config/settings/test.yml +++ b/config/settings/test.yml @@ -14,3 +14,5 @@ encoding: engine_adapter: test email: mailer: +zookeeper: + connection_str: "localhost:9985/configs" diff --git a/config/shoryuken.yml.erb b/config/shoryuken.yml.erb new file mode 100644 index 0000000000..835d09d120 --- /dev/null +++ b/config/shoryuken.yml.erb @@ -0,0 +1,6 @@ +--- +concurrency: 20 +queues: +<% queue_names.each do |queue_name| %> + - ['<%=queue_name%>', 1] +<% end %> \ No newline at end of file diff --git a/db/migrate/20220518181640_create_redirects.rb b/db/migrate/20220518181640_create_redirects.rb new file mode 100644 index 0000000000..3874954690 --- /dev/null +++ b/db/migrate/20220518181640_create_redirects.rb @@ -0,0 +1,9 @@ +class CreateRedirects < ActiveRecord::Migration[5.2] + def change + create_table :redirects, id: false do |t| + t.string :id, null: false, primary_key: true + t.string :item_target, null: false + t.string :embed_target, null: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index fba55dcffe..a686f36729 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -205,6 +205,11 @@ t.index ["user_id"], name: "index_playlists_on_user_id" end + create_table "redirects", id: :string, force: :cascade do |t| + t.string "item_target", null: false + t.string "embed_target" + end + create_table "role_maps", force: :cascade do |t| t.string "entry" t.integer "parent_id" diff --git a/docker-compose.yml b/docker-compose.yml index ee77c7a422..b76be2bf1d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,36 +1,43 @@ +--- version: '3.4' - volumes: - database: fedora: + db: + goaws: + minio: solr: - npms: - data: - -networks: - internal: - external: - services: db: &db-avalon image: postgres:14-alpine + restart: unless-stopped + read_only: true volumes: - - database:/data + - type: volume + source: db + target: /data + - type: volume + target: /tmp + - type: volume + target: /var/log + - type: volume + target: /var/run environment: - - PGDATA=/data - - POSTGRES_USER=postgres - - POSTGRES_DB=avalon - - POSTGRES_PASSWORD=password - networks: - internal: - db-test: - <<: *db-avalon - volumes: [] - - fedora: &fedora - image: avalonmediasystem/fedora:4.7.5 - depends_on: - - db + - PGDATA=/data + - POSTGRES_USER=docker + - POSTGRES_PASSWORD=d0ck3r + ports: + - 5433:5432 + - 5434:5432 + command: postgres -c max_connections=300 + healthcheck: + test: "CMD echo 'SELECT 1' | PGPASSWORD=d0ck3r psql --host 127.0.0.1 --username docker --dbname docker --quiet --no-align --tuples-only" + interval: 30s + timeout: 5s + retries: 3 + fedora: + image: samvera/fcrepo4:4.7.5 + restart: unless-stopped + read_only: true volumes: - fedora:/data environment: @@ -173,17 +180,37 @@ services: MINIO_ACCESS_KEY: minio MINIO_SECRET_KEY: minio123 volumes: - - data:/data + - type: volume + source: minio + target: /data + - type: volume + target: /root/.minio + - type: bind + source: $HOME/.dev_cert/dev.rdc.cert.pem + target: /root/.minio/certs/public.crt + - type: bind + source: $HOME/.dev_cert/dev.rdc.key.pem + target: /root/.minio/certs/private.key + - type: volume + target: /tmp + - type: volume + target: /var/log + - type: volume + target: /var/run ports: - 9000:9000 - 9090:9090 networks: - internal: - external: + default: + aliases: + - ${DEV_PREFIX}.dev.rdc.library.northwestern.edu healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + test: + - CMD + - curl + - https://${DEV_PREFIX}.dev.rdc.library.northwestern.edu:9001/minio/health/live interval: 30s - timeout: 20s + timeout: 5s retries: 3 createbuckets: diff --git a/extras/goaws/dev.yml b/extras/goaws/dev.yml new file mode 100644 index 0000000000..1b2eb1f3d7 --- /dev/null +++ b/extras/goaws/dev.yml @@ -0,0 +1,15 @@ +Local: + Host: localhost + Port: 4101 + Region: + AccountId: "100010001000" + LogToFile: false + LogFile: .st/goaws_messages.log + QueueAttributeDefaults: + VisibilityTimeout: 30 + ReceiveMessageWaitTimeSeconds: 0 + Queues: [] + Topics: [] + RandomLatency: + Min: 0 + Max: 0 diff --git a/extras/goaws/test.yml b/extras/goaws/test.yml new file mode 100644 index 0000000000..33989adc7b --- /dev/null +++ b/extras/goaws/test.yml @@ -0,0 +1,15 @@ +Local: + Host: localhost + Port: 4102 + Region: + AccountId: "100010001000" + LogToFile: false + LogFile: .st/goaws_messages.log + QueueAttributeDefaults: + VisibilityTimeout: 30 + ReceiveMessageWaitTimeSeconds: 0 + Queues: [] + Topics: [] + RandomLatency: + Min: 0 + Max: 0 diff --git a/lib/avalon/batch/ingest.rb b/lib/avalon/batch/ingest.rb index 29cd6d0c16..e6722a30a5 100644 --- a/lib/avalon/batch/ingest.rb +++ b/lib/avalon/batch/ingest.rb @@ -60,8 +60,11 @@ def process_valid_package(package: nil) # Otherwise this should be set # Validate the package if a side package is passed in unless package.nil? - return unless valid_package? @current_package = package + unless valid_package? + @current_package = nil + return + end end # We have a valid batch so we can go ahead and delete the manifest file diff --git a/lib/avalon/batch/package.rb b/lib/avalon/batch/package.rb index 9b26869c29..b928010a59 100644 --- a/lib/avalon/batch/package.rb +++ b/lib/avalon/batch/package.rb @@ -36,7 +36,7 @@ def title def user @user ||= - User.where(Devise.authentication_keys.first => @manifest.email).first || + User.find_by_devise_authentication_keys(@manifest.email).first || User.where(username: @manifest.email).first || User.where(email: @manifest.email).first @user diff --git a/lib/avalon/bib_retriever/sru.rb b/lib/avalon/bib_retriever/sru.rb index 5b766715b7..ca76bf4464 100644 --- a/lib/avalon/bib_retriever/sru.rb +++ b/lib/avalon/bib_retriever/sru.rb @@ -35,7 +35,7 @@ def get_record(bib_id) def url_for(query, bib_id) uri = Addressable::URI.parse config['url'] - query_param = Addressable::URI.escape(query % { bib_id: bib_id.to_s }) + query_param = Addressable::URI.escape_component(query % { bib_id: bib_id.to_s }) uri.query = "version=1.1&operation=searchRetrieve&maximumRecords=1&recordSchema=marcxml&query=#{query_param}" uri.to_s end diff --git a/lib/tasks/avalon.rake b/lib/tasks/avalon.rake index 9fc749440d..a55b1408e8 100644 --- a/lib/tasks/avalon.rake +++ b/lib/tasks/avalon.rake @@ -145,7 +145,7 @@ namespace :avalon do username = ENV['avalon_username'].dup groups = Avalon::RoleControls.user_roles username - User.where(Devise.authentication_keys.first => username).destroy_all + User.find_by_devise_authentication_keys(username).destroy_all groups.each do |group| Avalon::RoleControls.remove_user_role(username, group) end @@ -274,7 +274,7 @@ namespace :avalon do # Save existing playlist/item/marker data for users being imported puts "Compiling existing avalon marker data" usernames = import_json.collect{|user|user['username']} - userids = User.where(Devise.authentication_keys.first => usernames).collect(&:id) + userids = User.find_by_devise_authentication_keys(usernames).collect(&:id) userids.each do |user_id| print "." playlist = Playlist.where(user_id: user_id, title:'Variations Bookmarks').first diff --git a/lib/tasks/avr.rake b/lib/tasks/avr.rake new file mode 100644 index 0000000000..ccf1e17c0a --- /dev/null +++ b/lib/tasks/avr.rake @@ -0,0 +1,32 @@ +namespace :avr do + desc "Prepare the AVR environment" + task :setup do + ['zookeeper:upload', 'zookeeper:create', 'db:create', 'db:migrate'].each do |task| + Rake::Task[task].invoke + end + + begin + Rake::Task['avalon:reindex'].invoke + rescue + end + end + + desc "Completely reset the AVR environment" + task :reset do + Rake::Task['avr:setup'].invoke + + ENV['CONFIRM'] = 'yes' + $stderr.puts("Wiping DB, Fedora, Solr, and Redis") + Rake::Task['avalon:wipeout'].invoke + + $stderr.puts("Emptying derivatives bucket") + Aws::S3::Bucket.new(Settings.encoding.derivative_bucket).objects.each(&:delete) + $stderr.puts("Emptying masterfiles bucket") + Aws::S3::Bucket.new(Settings.encoding.masterfile_bucket).objects.each(&:delete) + + $stderr.puts("Recreating Fedora root resource path") + ActiveFedora.fedora.connection.tap do |conn| + conn.head(conn.root_resource_path) + end + end +end \ No newline at end of file diff --git a/lib/tasks/shoryuken.rake b/lib/tasks/shoryuken.rake new file mode 100644 index 0000000000..717b874da3 --- /dev/null +++ b/lib/tasks/shoryuken.rake @@ -0,0 +1,36 @@ +# ActiveJob and ActionMailer handle queue names and prefixes differently, so +# we need to make allowances for the fact that ActiveJob queue names will +# already have their prefixes while ActionMailer queue names will not + +require 'aws-sdk-sqs' + +namespace :shoryuken do + desc "Create shoryuken config file" + task create_config: :environment do + Rails.root.glob('app/jobs/**/*').each { |file| load file } + active_job_config = Rails.application.config.active_job + prefix = active_job_config.queue_name_prefix.to_s + active_job_config.queue_name_delimiter.to_s + queue_names = ActiveJob::Base.descendants.collect do |job_class| + queue_name = job_class.queue_name || 'default' + queue_name = queue_name.call if queue_name.respond_to?(:call) + queue_name = 'default' if queue_name.nil? + queue_name = prefix + queue_name unless queue_name.start_with?(prefix) + queue_name + end.uniq + template = ERB.new(File.read(Rails.root.join('config/shoryuken.yml.erb')), trim_mode: '<>') + File.open(Rails.root.join('config/shoryuken.yml'), 'w') do |config_file| + rendered = template.result(binding) + config_file.write(rendered) + end + end + + desc "Create SQS queues for shoryuken" + task create_queues: :environment do + sqs = Aws::SQS::Client.new + shoryuken_config = YAML.load(File.read(Rails.root.join('config/shoryuken.yml'))) + shoryuken_config["queues"].each do |queue_name, _count| + sqs.create_queue(queue_name: queue_name) + $stderr.puts "Created #{queue_name}" + end + end +end \ No newline at end of file diff --git a/lib/tasks/wipeout.rake b/lib/tasks/wipeout.rake index ce948eac6d..57a00db492 100644 --- a/lib/tasks/wipeout.rake +++ b/lib/tasks/wipeout.rake @@ -12,6 +12,8 @@ # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- +require 'active_fedora/cleaner' + def wipeout_fedora ActiveFedora::Cleaner.clean! end diff --git a/package.json b/package.json index 4f5aa1441a..695229d5ac 100644 --- a/package.json +++ b/package.json @@ -59,5 +59,6 @@ "start-collection-view": "webpack-dev-server --mode development --config config/webpack/collection_view.js --host 0.0.0.0", "cypress:open": "cypress open -C spec/cypress/cypress.config.js", "cypress:run": "cypress run -C spec/cypress/cypress.config.js" - } + }, + "packageManager": "yarn@1.2.2" } diff --git a/solr/config/schema.xml b/solr/config/schema.xml index dbd9cbc8d8..3f6d39aa85 100644 --- a/solr/config/schema.xml +++ b/solr/config/schema.xml @@ -165,6 +165,7 @@ + - - + + @@ -349,8 +352,10 @@ +