INITIAL GOSH DANG COMMIT :3333

This commit is contained in:
24c02 2025-05-31 23:25:41 -04:00
commit c405c68a7d
657 changed files with 30205 additions and 0 deletions

58
.annotaterb.yml Normal file
View file

@ -0,0 +1,58 @@
---
:position: before
:position_in_additional_file_patterns: before
:position_in_class: before
:position_in_factory: before
:position_in_fixture: before
:position_in_routes: before
:position_in_serializer: before
:position_in_test: before
:classified_sort: true
:exclude_controllers: true
:exclude_factories: false
:exclude_fixtures: false
:exclude_helpers: true
:exclude_scaffolds: true
:exclude_serializers: false
:exclude_sti_subclasses: false
:exclude_tests: false
:force: false
:format_markdown: false
:format_rdoc: false
:format_yard: false
:frozen: false
:ignore_model_sub_dir: false
:ignore_unknown_models: false
:include_version: false
:show_check_constraints: false
:show_complete_foreign_keys: false
:show_foreign_keys: true
:show_indexes: true
:simple_indexes: false
:sort: false
:timestamp: false
:trace: false
:with_comment: true
:with_column_comments: true
:with_table_comments: true
:active_admin: false
:command:
:debug: false
:hide_default_column_types: ''
:hide_limit_column_types: ''
:ignore_columns:
:ignore_routes:
:models: true
:routes: false
:skip_on_db_migrate: false
:target_action: :do_annotations
:wrapper:
:wrapper_close:
:wrapper_open:
:classes_default_to_s: []
:additional_file_patterns: []
:model_dir:
- app/models
:require: []
:root_dir:
- ''

51
.dockerignore Normal file
View file

@ -0,0 +1,51 @@
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files.
# Ignore git directory.
/.git/
/.gitignore
# Ignore bundler config.
/.bundle
# Ignore all environment files.
/.env*
# Ignore all default key files.
/config/master.key
/config/credentials/*.key
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore pidfiles, but keep the directory.
/tmp/pids/*
!/tmp/pids/.keep
# Ignore storage (uploaded files in development and any SQLite databases).
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/.keep
# Ignore assets.
/node_modules/
/app/assets/builds/*
!/app/assets/builds/.keep
/public/assets
# Ignore CI service files.
/.github
# Ignore Kamal files.
/config/deploy*.yml
/.kamal
# Ignore development files
/.devcontainer
# Ignore Docker-related files
/.dockerignore
/Dockerfile*

9
.gitattributes vendored Normal file
View file

@ -0,0 +1,9 @@
# See https://git-scm.com/docs/gitattributes for more about git attribute files.
# Mark the database schema as having been generated.
db/schema.rb linguist-generated
# Mark any vendored files as having been vendored.
vendor/* linguist-vendored
config/credentials/*.yml.enc diff=rails_credentials
config/credentials.yml.enc diff=rails_credentials

12
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: bundler
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

54
.gitignore vendored Normal file
View file

@ -0,0 +1,54 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# Temporary files generated by your text editor or operating system
# belong in git's global ignore instead:
# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore`
# Ignore bundler config.
/.bundle
# Ignore all environment files.
/.env*
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore pidfiles, but keep the directory.
/tmp/pids/*
!/tmp/pids/
!/tmp/pids/.keep
# Ignore storage (uploaded files in development and any SQLite databases).
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/
!/tmp/storage/.keep
/public/assets
# Ignore master key for decrypting credentials and more.
/config/master.key
/app/assets/builds/*
!/app/assets/builds/.keep
/node_modules
/config/credentials/production.key
**/.DS_Store
# Vite Ruby
/public/vite*
node_modules
# Vite uses dotenv and suggests to ignore local-only env files. See
# https://vitejs.dev/guide/env-and-mode.html#env-files
*.local
*.key
app/frontend/images/template_previews/*

View file

@ -0,0 +1,3 @@
#!/bin/sh
echo "Docker set up on $KAMAL_HOSTS..."

View file

@ -0,0 +1,3 @@
#!/bin/sh
echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..."

14
.kamal/hooks/post-deploy.sample Executable file
View file

@ -0,0 +1,14 @@
#!/bin/sh
# A sample post-deploy hook
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
# KAMAL_RUNTIME
echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"

View file

@ -0,0 +1,3 @@
#!/bin/sh
echo "Rebooted kamal-proxy on $KAMAL_HOSTS"

View file

@ -0,0 +1,3 @@
#!/bin/sh
echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..."

51
.kamal/hooks/pre-build.sample Executable file
View file

@ -0,0 +1,51 @@
#!/bin/sh
# A sample pre-build hook
#
# Checks:
# 1. We have a clean checkout
# 2. A remote is configured
# 3. The branch has been pushed to the remote
# 4. The version we are deploying matches the remote
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
if [ -n "$(git status --porcelain)" ]; then
echo "Git checkout is not clean, aborting..." >&2
git status --porcelain >&2
exit 1
fi
first_remote=$(git remote)
if [ -z "$first_remote" ]; then
echo "No git remote set, aborting..." >&2
exit 1
fi
current_branch=$(git branch --show-current)
if [ -z "$current_branch" ]; then
echo "Not on a git branch, aborting..." >&2
exit 1
fi
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
if [ -z "$remote_head" ]; then
echo "Branch not pushed to remote, aborting..." >&2
exit 1
fi
if [ "$KAMAL_VERSION" != "$remote_head" ]; then
echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
exit 1
fi
exit 0

47
.kamal/hooks/pre-connect.sample Executable file
View file

@ -0,0 +1,47 @@
#!/usr/bin/env ruby
# A sample pre-connect check
#
# Warms DNS before connecting to hosts in parallel
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
# KAMAL_RUNTIME
hosts = ENV["KAMAL_HOSTS"].split(",")
results = nil
max = 3
elapsed = Benchmark.realtime do
results = hosts.map do |host|
Thread.new do
tries = 1
begin
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
rescue SocketError
if tries < max
puts "Retrying DNS warmup: #{host}"
tries += 1
sleep rand
retry
else
puts "DNS warmup failed: #{host}"
host
end
end
tries
end
end.map(&:value)
end
retries = results.sum - hosts.size
nopes = results.count { |r| r == max }
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]

109
.kamal/hooks/pre-deploy.sample Executable file
View file

@ -0,0 +1,109 @@
#!/usr/bin/env ruby
# A sample pre-deploy hook
#
# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
#
# Fails unless the combined status is "success"
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_COMMAND
# KAMAL_SUBCOMMAND
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
# Only check the build status for production deployments
if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
exit 0
end
require "bundler/inline"
# true = install gems so this is fast on repeat invocations
gemfile(true, quiet: true) do
source "https://rubygems.org"
gem "octokit"
gem "faraday-retry"
end
MAX_ATTEMPTS = 72
ATTEMPTS_GAP = 10
def exit_with_error(message)
$stderr.puts message
exit 1
end
class GithubStatusChecks
attr_reader :remote_url, :git_sha, :github_client, :combined_status
def initialize
@remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
@git_sha = `git rev-parse HEAD`.strip
@github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
refresh!
end
def refresh!
@combined_status = github_client.combined_status(remote_url, git_sha)
end
def state
combined_status[:state]
end
def first_status_url
first_status = combined_status[:statuses].find { |status| status[:state] == state }
first_status && first_status[:target_url]
end
def complete_count
combined_status[:statuses].count { |status| status[:state] != "pending"}
end
def total_count
combined_status[:statuses].count
end
def current_status
if total_count > 0
"Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
else
"Build not started..."
end
end
end
$stdout.sync = true
puts "Checking build status..."
attempts = 0
checks = GithubStatusChecks.new
begin
loop do
case checks.state
when "success"
puts "Checks passed, see #{checks.first_status_url}"
exit 0
when "failure"
exit_with_error "Checks failed, see #{checks.first_status_url}"
when "pending"
attempts += 1
end
exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
puts checks.current_status
sleep(ATTEMPTS_GAP)
checks.refresh!
end
rescue Octokit::NotFound
exit_with_error "Build status could not be found"
end

View file

@ -0,0 +1,3 @@
#!/bin/sh
echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."

17
.kamal/secrets Normal file
View file

@ -0,0 +1,17 @@
# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
# Example of extracting secrets from 1password (or another compatible pw manager)
# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS})
# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS})
# Use a GITHUB_TOKEN if private repositories are needed for the image
# GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
# Grab the registry password from ENV
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
# Improve security by using a password manager. Never check config/master.key into git!
RAILS_MASTER_KEY=$(cat config/master.key)

1
.node-version Normal file
View file

@ -0,0 +1 @@
23.6.0

8
.rubocop.yml Normal file
View file

@ -0,0 +1,8 @@
# Omakase Ruby styling for Rails
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
# Overwrite or add rules to create your own house style
#
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
# Layout/SpaceInsideArrayLiteralBrackets:
# Enabled: false

1
.ruby-version Normal file
View file

@ -0,0 +1 @@
3.4.4

82
Dockerfile Normal file
View file

@ -0,0 +1,82 @@
# syntax=docker/dockerfile:1
# check=error=true
# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t theseus .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name theseus theseus
# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.4
FROM docker.io/library/ruby:$RUBY_VERSION-slim
# Set production environment
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
# Install base packages and build dependencies
RUN apt-get update -qq && apt-get install --no-install-recommends -y \
curl \
libjemalloc2 \
libvips \
postgresql-client \
wget \
build-essential \
git \
libpq-dev \
node-gyp \
pkg-config \
python-is-python3 \
imagemagick \
libmagickwand-dev \
ghostscript \
libyaml-dev \
chromium
RUN sed -i '/disable ghostscript format types/,+6d' /etc/ImageMagick-6/policy.xml
# Install Node.js and Yarn
ARG NODE_VERSION=23.6.0
ARG YARN_VERSION=1.22.22
ENV PATH=/usr/local/node/bin:$PATH
RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
/tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
npm install -g yarn@$YARN_VERSION && \
rm -rf /tmp/node-build-master
# Rails app lives here
WORKDIR /rails
# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile
# Install node modules
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
# Copy application code
COPY . .
# Precompile bootsnap code and assets
RUN bundle exec bootsnap precompile app/ lib/ && \
SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile && \
rm -rf node_modules
# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
USER 1000:1000
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]

171
Gemfile Normal file
View file

@ -0,0 +1,171 @@
source "https://rubygems.org"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.0.1"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Use postgresql as the database for Active Record
gem "pg", "~> 1.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails]
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder", "~> 2.13"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]
# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable
gem "solid_cache"
gem "solid_queue"
gem "solid_cable"
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false
# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]
gem "kamal", require: false
# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
gem "thruster", require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
# Static analysis for security vulnerabilities [https://brakemanscanner.org/]
gem "brakeman", require: false
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
gem "rubocop-rails-omakase", require: false
end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem "web-console"
end
group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara"
gem "selenium-webdriver"
end
gem "foreman", "~> 0.88.1"
gem "dotenv-rails", "~> 3.1"
gem "net-http", "~> 0.6.0"
gem "http", "~> 5.2"
gem "annotaterb", "~> 4.14"
gem "aasm", "~> 5.5"
gem "norairrecord", "~> 0.3.0"
gem "filterrific", "~> 5.2"
gem "hashid-rails", "~> 1.4"
gem "csv", "~> 3.3"
gem "faraday", "~> 2.12"
gem "oauth2", "~> 2.0"
gem "snail", "~> 2.3"
gem "easypost", "~> 6.4"
gem "pundit", "~> 2.5"
gem "select2-rails", "~> 4.0"
gem "jquery-rails", "~> 4.6"
gem "country-select", "~> 1.2"
gem "countries", "~> 7.1"
gem "good_job", "~> 4.9"
gem "awesome_print", "~> 1.9"
gem "cocoon", "~> 1.2"
gem "administrate", "~> 0.19.0"
gem "slim-rails", "~> 3.7"
group :development do
gem "letter_opener_web", "~> 3.0"
end
gem "prawn", "~> 2.5"
gem "usps_intelligent_barcode", "~> 1.0"
gem "rqrcode", "~> 2.2"
gem "kaminari", "~> 1.2"
gem "aws-sdk-s3", require: false
gem "acts-as-taggable-array-on", "~> 0.7.0"
gem "selectize-rails", "~> 0.12.6"
gem "ivymeter", "~> 0.1.0"
gem "slack-notifier", "~> 2.4"
gem "nokogiri", "~> 1.18"
gem "vite_rails"
gem "blazer", "~> 3.3"
gem "redis", "~> 5.4"
gem "valid_email2", "~> 7.0"
gem "sssecrets", "~> 1.0"
gem "lockbox", "~> 2.0"
gem "blind_index", "~> 2.7"
gem "ruby-openai", "~> 8.1"
gem "parallel", "~> 1.26"
gem "honeybadger", "~> 5.28"
gem "rmagick", "~> 5.3"
gem "jb", "~> 0.8.2"
gem "ferrum_pdf", "~> 0.3.0"
gem "literal", "~> 1.7"
gem "phlex-rails", "~> 2.2"
gem "xsv", "~> 1.3"

702
Gemfile.lock Normal file
View file

@ -0,0 +1,702 @@
GEM
remote: https://rubygems.org/
specs:
aasm (5.5.0)
concurrent-ruby (~> 1.0)
actioncable (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
actionmailer (8.0.2)
actionpack (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.2)
actionview (= 8.0.2)
activesupport (= 8.0.2)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.2)
actionpack (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.2)
activesupport (= 8.0.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.2)
activesupport (= 8.0.2)
globalid (>= 0.3.6)
activemodel (8.0.2)
activesupport (= 8.0.2)
activerecord (8.0.2)
activemodel (= 8.0.2)
activesupport (= 8.0.2)
timeout (>= 0.4.0)
activestorage (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activesupport (= 8.0.2)
marcel (~> 1.0)
activesupport (8.0.2)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
acts-as-taggable-array-on (0.7.0)
activerecord (>= 5.2)
activesupport (>= 5.2)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
administrate (0.19.0)
actionpack (>= 5.0)
actionview (>= 5.0)
activerecord (>= 5.0)
jquery-rails (>= 4.0)
kaminari (>= 1.0)
sassc-rails (~> 2.1)
selectize-rails (~> 0.6)
andand (1.3.3)
annotaterb (4.14.0)
argon2-kdf (0.3.1)
fiddle
ast (2.4.3)
awesome_print (1.9.2)
aws-eventstream (1.3.2)
aws-partitions (1.1106.0)
aws-sdk-core (3.224.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.101.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.186.1)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
base64 (0.2.0)
bcrypt_pbkdf (1.1.1)
bcrypt_pbkdf (1.1.1-arm64-darwin)
bcrypt_pbkdf (1.1.1-x86_64-darwin)
benchmark (0.4.0)
bigdecimal (3.1.9)
bindex (0.8.1)
blazer (3.3.0)
activerecord (>= 7.1)
chartkick (>= 5)
csv
railties (>= 7.1)
safely_block (>= 0.4)
blind_index (2.7.0)
activesupport (>= 7.1)
argon2-kdf (>= 0.2)
bootsnap (1.18.6)
msgpack (~> 1.2)
brakeman (7.0.2)
racc
builder (3.3.0)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
chartkick (5.1.5)
childprocess (5.1.0)
logger (~> 1.5)
chunky_png (1.4.0)
cocoon (1.2.15)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
countries (7.1.1)
unaccent (~> 0.3)
country-select (1.2.1)
crass (1.0.6)
csv (3.3.4)
date (3.4.1)
debug (1.10.0)
irb (~> 1.10)
reline (>= 0.3.8)
domain_name (0.6.20240107)
dotenv (3.1.8)
dotenv-rails (3.1.8)
dotenv (= 3.1.8)
railties (>= 6.1)
drb (2.2.1)
dry-cli (1.2.0)
easypost (6.4.1)
ed25519 (1.4.0)
erb (5.0.1)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
event_stream_parser (1.0.0)
faraday (2.13.1)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (3.4.0)
net-http (>= 0.5.0)
faraday-net_http_persistent (2.3.0)
faraday (~> 2.5)
net-http-persistent (>= 4.0.4, < 5)
ferrum (0.17.1)
addressable (~> 2.5)
base64 (~> 0.2)
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
ferrum_pdf (0.3.0)
ferrum (~> 0.15)
rails (>= 6.0.0)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
ffi (1.17.2-arm-linux-musl)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
ffi (1.17.2-x86_64-linux-musl)
ffi-compiler (1.3.2)
ffi (>= 1.15.5)
rake
fiddle (1.1.8)
filterrific (5.2.7)
foreman (0.88.1)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
good_job (4.10.1)
activejob (>= 6.1.0)
activerecord (>= 6.1.0)
concurrent-ruby (>= 1.3.1)
fugit (>= 1.11.0)
railties (>= 6.1.0)
thor (>= 1.0.0)
hashid-rails (1.4.1)
activerecord (>= 4.0)
hashids (~> 1.0)
hashids (1.0.6)
hashie (5.0.0)
honeybadger (5.28.0)
logger
ostruct
http (5.2.0)
addressable (~> 2.8)
base64 (~> 0.1)
http-cookie (~> 1.0)
http-form_data (~> 2.2)
llhttp-ffi (~> 0.5.0)
http-cookie (1.0.8)
domain_name (~> 0.5)
http-form_data (2.3.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
io-console (0.8.0)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
ivymeter (0.1.0)
jb (0.8.2)
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jmespath (1.6.2)
jquery-rails (4.6.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.12.0)
jwt (2.10.1)
base64
kamal (2.6.1)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)
concurrent-ruby (~> 1.2)
dotenv (~> 3.1)
ed25519 (~> 1.4)
net-ssh (~> 7.3)
sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3)
zeitwerk (>= 2.6.18, < 3.0)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
kaminari-activerecord (= 1.2.2)
kaminari-core (= 1.2.2)
kaminari-actionview (1.2.2)
actionview
kaminari-core (= 1.2.2)
kaminari-activerecord (1.2.2)
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
language_server-protocol (3.17.0.5)
launchy (3.1.1)
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (3.0.0)
actionmailer (>= 6.1)
letter_opener (~> 1.9)
railties (>= 6.1)
rexml
lint_roller (1.1.0)
literal (1.7.1)
zeitwerk
llhttp-ffi (0.5.1)
ffi-compiler (~> 1.0)
rake (~> 13.0)
lockbox (2.0.1)
logger (1.7.0)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.4)
matrix (0.4.2)
memoizer (1.0.3)
mini_mime (1.1.5)
minitest (5.25.5)
msgpack (1.8.0)
multi_xml (0.7.2)
bigdecimal (~> 3.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
net-http (0.6.0)
uri
net-http-persistent (4.0.5)
connection_pool (~> 2.2)
net-imap (0.5.8)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-scp (4.1.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.1)
net-protocol
net-ssh (7.3.0)
nio4r (2.7.4)
nokogiri (1.18.8-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.8-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.8-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-musl)
racc (~> 1.4)
norairrecord (0.3.0)
faraday (>= 1.0, < 3.0)
faraday-net_http_persistent
net-http-persistent
oauth2 (2.0.10)
faraday (>= 0.17.3, < 4.0)
jwt (>= 1.0, < 4.0)
logger (~> 1.2)
multi_xml (~> 0.5)
rack (>= 1.2, < 4)
snaky_hash (~> 2.0)
version_gem (>= 1.1.8, < 3)
observer (0.1.2)
ostruct (0.6.1)
parallel (1.27.0)
parser (3.3.8.0)
ast (~> 2.4.1)
racc
pdf-core (0.10.0)
pg (1.5.9)
phlex (2.2.1)
zeitwerk (~> 2.7)
phlex-rails (2.2.0)
phlex (~> 2.2.1)
railties (>= 7.1, < 9)
pkg-config (1.6.2)
pp (0.6.2)
prettyprint
prawn (2.5.0)
matrix (~> 0.4)
pdf-core (~> 0.10.0)
ttfunk (~> 1.8)
prettyprint (0.2.0)
prism (1.4.0)
propshaft (1.1.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
railties (>= 7.0.0)
psych (5.2.6)
date
stringio
public_suffix (6.0.2)
puma (6.6.0)
nio4r (~> 2.0)
pundit (2.5.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.15)
rack-proxy (0.7.7)
rack
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
rails (8.0.2)
actioncable (= 8.0.2)
actionmailbox (= 8.0.2)
actionmailer (= 8.0.2)
actionpack (= 8.0.2)
actiontext (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activemodel (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
bundler (>= 1.15.0)
railties (= 8.0.2)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
railties (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rdoc (6.14.0)
erb
psych (>= 4.0.0)
redis (5.4.0)
redis-client (>= 0.22.0)
redis-client (0.24.0)
connection_pool
regexp_parser (2.10.0)
reline (0.6.1)
io-console (~> 0.5)
rexml (3.4.1)
rmagick (5.5.0)
observer (~> 0.1)
pkg-config (~> 1.4)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rubocop (1.75.6)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.44.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.44.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-performance (1.25.0)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.32.0)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rails-omakase (1.1.0)
rubocop (>= 1.72)
rubocop-performance (>= 1.24)
rubocop-rails (>= 2.30)
ruby-openai (8.1.0)
event_stream_parser (>= 0.3.0, < 2.0.0)
faraday (>= 1)
faraday-multipart (>= 1)
ruby-progressbar (1.13.0)
rubyzip (2.4.1)
safely_block (0.4.1)
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
securerandom (0.4.1)
select2-rails (4.0.13)
selectize-rails (0.12.6)
selenium-webdriver (4.32.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
slack-notifier (2.4.0)
slim (5.2.1)
temple (~> 0.10.0)
tilt (>= 2.1.0)
slim-rails (3.7.0)
actionpack (>= 3.1)
railties (>= 3.1)
slim (>= 3.0, < 6.0, != 5.0.0)
snail (2.3.0)
activesupport
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
solid_cable (3.0.8)
actioncable (>= 7.2)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_cache (1.0.7)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_queue (1.1.5)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
fugit (~> 1.11.0)
railties (>= 7.1)
thor (~> 1.3.1)
sprockets (4.2.2)
concurrent-ruby (~> 1.0)
logger
rack (>= 2.2.4, < 4)
sprockets-rails (3.5.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
sshkit (1.24.0)
base64
logger
net-scp (>= 1.1.2)
net-sftp (>= 2.1.2)
net-ssh (>= 2.8.0)
ostruct
sssecrets (1.0.1)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.7)
temple (0.10.3)
thor (1.3.2)
thruster (0.1.13)
thruster (0.1.13-aarch64-linux)
thruster (0.1.13-arm64-darwin)
thruster (0.1.13-x86_64-darwin)
thruster (0.1.13-x86_64-linux)
tilt (2.6.0)
timeout (0.4.3)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
turbo-rails (2.0.13)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unaccent (0.4.0)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
useragent (0.16.11)
usps_intelligent_barcode (1.0.0)
andand (~> 1.3)
memoizer (~> 1.0)
valid_email2 (7.0.13)
activemodel (>= 6.0)
mail (~> 2.5)
version_gem (1.1.8)
vite_rails (3.0.19)
railties (>= 5.1, < 9)
vite_ruby (~> 3.0, >= 3.2.2)
vite_ruby (3.9.2)
dry-cli (>= 0.7, < 2)
logger (~> 1.6)
mutex_m
rack-proxy (~> 0.6, >= 0.6.1)
zeitwerk (~> 2.2)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.7.7)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
xsv (1.3.2)
rubyzip (>= 1.3, < 3)
zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
x86_64-darwin
x86_64-linux
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
aasm (~> 5.5)
acts-as-taggable-array-on (~> 0.7.0)
administrate (~> 0.19.0)
annotaterb (~> 4.14)
awesome_print (~> 1.9)
aws-sdk-s3
blazer (~> 3.3)
blind_index (~> 2.7)
bootsnap
brakeman
capybara
cocoon (~> 1.2)
countries (~> 7.1)
country-select (~> 1.2)
csv (~> 3.3)
debug
dotenv-rails (~> 3.1)
easypost (~> 6.4)
faraday (~> 2.12)
ferrum_pdf (~> 0.3.0)
filterrific (~> 5.2)
foreman (~> 0.88.1)
good_job (~> 4.9)
hashid-rails (~> 1.4)
honeybadger (~> 5.28)
http (~> 5.2)
ivymeter (~> 0.1.0)
jb (~> 0.8.2)
jbuilder (~> 2.13)
jquery-rails (~> 4.6)
kamal
kaminari (~> 1.2)
letter_opener_web (~> 3.0)
literal (~> 1.7)
lockbox (~> 2.0)
net-http (~> 0.6.0)
nokogiri (~> 1.18)
norairrecord (~> 0.3.0)
oauth2 (~> 2.0)
parallel (~> 1.26)
pg (~> 1.1)
phlex-rails (~> 2.2)
prawn (~> 2.5)
propshaft
puma (>= 5.0)
pundit (~> 2.5)
rails (~> 8.0.1)
redis (~> 5.4)
rmagick (~> 5.3)
rqrcode (~> 2.2)
rubocop-rails-omakase
ruby-openai (~> 8.1)
select2-rails (~> 4.0)
selectize-rails (~> 0.12.6)
selenium-webdriver
slack-notifier (~> 2.4)
slim-rails (~> 3.7)
snail (~> 2.3)
solid_cable
solid_cache
solid_queue
sssecrets (~> 1.0)
stimulus-rails
thruster
turbo-rails
tzinfo-data
usps_intelligent_barcode (~> 1.0)
valid_email2 (~> 7.0)
vite_rails
web-console
xsv (~> 1.3)
BUNDLED WITH
2.6.9

2
Procfile.dev Normal file
View file

@ -0,0 +1,2 @@
web: env RUBY_DEBUG_OPEN=true bin/rails server
vite: bin/vite dev

16
README.md Normal file
View file

@ -0,0 +1,16 @@
# hack club's shiny new mail system!
it's open source now i guess
getting this running if you're not me or production is probably kind of a bear!
i will write seeds and a way to log in without setting up a slack....at...some point.....
(possibly not soon due to time constraints)
if you wanna hack on this and contribute, dm me (@nora) on slack, i'd love the help!
a lot of the design decisions and code are not great. they will be incrementally improved someday. (you can look towards issues for some of that)
thanks for being here!
~nora <3

6
Rakefile Normal file
View file

@ -0,0 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative "config/application"
Rails.application.load_tasks

0
app/assets/builds/.keep Normal file
View file

View file

15
app/components/base.rb Normal file
View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class Components::Base < Phlex::HTML
include Components
# Include any helpers you want to be available across all components
include Phlex::Rails::Helpers::Routes
if Rails.env.development?
def before_template
comment { "Before #{self.class.name}" }
super
end
end
end

View file

@ -0,0 +1,46 @@
module Admin
class AddressesController < Admin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end

View file

@ -0,0 +1,21 @@
# All Administrate controllers inherit from this
# `Administrate::ApplicationController`, making it the ideal place to put
# authentication logic or other before_actions.
#
# If you want to add pagination or other controller-level concerns,
# you're free to overwrite the RESTful controller actions.
module Admin
class ApplicationController < Administrate::ApplicationController
before_action :authenticate_admin
def authenticate_admin
redirect_to root_path, alert: "you can't do that!" unless current_user&.admin?
end
helper_method :current_user
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end
end
end

View file

@ -0,0 +1,46 @@
module Admin
class CommonTagsController < Admin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end

View file

@ -0,0 +1,46 @@
module Admin
class ReturnAddressesController < Admin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end

View file

@ -0,0 +1,46 @@
module Admin
class SourceTagsController < Admin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end

View file

@ -0,0 +1,46 @@
module Admin
class UsersController < Admin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end

View file

@ -0,0 +1,46 @@
module Admin
class USPS::MailerIdsController < Admin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end

View file

@ -0,0 +1,48 @@
module Admin
module USPS
class PaymentAccountsController < Admin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end
end

View file

@ -0,0 +1,48 @@
module Admin
module Warehouse
class LineItemsController < Admin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end
end

View file

@ -0,0 +1,49 @@
module Admin
module Warehouse
class OrdersController < Admin::ApplicationController
include Administrate::ArrayParameterHandler
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
def find_resource(param)
::Warehouse::Order.find_by!(hc_id: param)
end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end
end

View file

@ -0,0 +1,48 @@
module Admin
module Warehouse
class PurposeCodesController < Admin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end
end

View file

@ -0,0 +1,48 @@
module Admin
module Warehouse
class SKUsController < Admin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end
end

View file

@ -0,0 +1,48 @@
module Admin
module Warehouse
class TemplatesController < Admin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
def find_resource(param)
Warehouse::Template.find(param)
end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes(action_name)).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
end
end
end

View file

@ -0,0 +1,59 @@
module API
module V1
class ApplicationController < ActionController::API
prepend_view_path Rails.root.join("app/views/api/v1")
attr_reader :current_user
before_action :authenticate!
before_action :set_expand
before_action :set_pii
include Pundit::Authorization
include ActionController::HttpAuthentication::Token::ControllerMethods
rescue_from Pundit::NotAuthorizedError do |e|
render json: { error: "not_authorized" }, status: :forbidden
end
rescue_from ActiveRecord::RecordNotFound do |e|
render json: { error: "resource_not_found", message: ("Couldn't locate that #{e.model.constantize.model_name.human}." if e.model) }.compact_blank, status: :not_found
end
rescue_from ActiveRecord::RecordInvalid do |e|
render json: { error: "validation_error", messages: e.record.errors.full_messages }, status: :bad_request
end
rescue_from ActiveRecord::RecordNotUnique do |e|
render json: { error: "idempotency_error", messages: ["a record by that idempotency key already exists!"] }, status: :bad_request
end
private
def set_expand
@expand = params[:expand].to_s.split(",").map { |e| e.strip.to_sym }
end
def set_pii
@pii = current_token&.pii?
end
def authenticate!
@current_token = authenticate_with_http_token { |t, _options| APIKey.find_by(token: t) }
unless @current_token&.active?
return render json: { error: "invalid_auth" }, status: :unauthorized
end
@current_user = if current_token&.may_impersonate? && params[:impersonate].present?
begin
User.find_by!(slack_id: params[:impersonate])
rescue ActiveRecord::RecordNotFound
render json: { error: "impersonate_error", message: "couldn't find that user" }, status: :bad_request
end
else
current_token&.user
end
end
attr_reader :current_token
end
end
end

View file

@ -0,0 +1,128 @@
module API
module V1
class LetterQueuesController < ApplicationController
before_action :set_letter_queue, only: [:show, :create_letter]
before_action :set_instant_letter_queue, only: [:create_instant_letter, :queued]
rescue_from ActiveRecord::RecordNotFound do |e|
render json: { error: "Queue not found" }, status: :not_found
end
rescue_from ActiveRecord::RecordInvalid do |e|
Honeybadger.notify(e)
render json: {
error: "Validation failed",
details: e.record.errors.full_messages,
}, status: :unprocessable_entity
end
def show
authorize @letter_queue
end
# this should REALLY be websockets or some shit
# but it's not, so we're just going to poll
# i'm not braining well enough to do it right anytime soon
def queued
# authorize @letter_queue, policy_class: Letter::QueuePolicy
raise Pundit::NotAuthorizedError unless current_token&.pii?
return render json: { error: "no" } unless @letter_queue.is_a?(Letter::InstantQueue)
@expand = [:label]
@letters = @letter_queue.letters.pending
end
def create_letter
authorize @letter_queue
# Normalize country name using FrickinCountryNames
country = FrickinCountryNames.find_country(letter_params[:address][:country])
if country.nil?
render json: { error: "couldn't figure out country name #{letter_params[:address][:country]}" }, status: :unprocessable_entity
return
end
# Create address with normalized country
address_params = letter_params[:address].merge(country: country.alpha2)
# Normalize state name to abbreviation
address_params[:state] = FrickinCountryNames.normalize_state(country, address_params[:state])
addy = Address.new(address_params)
@letter = @letter_queue.create_letter!(
addy,
letter_params.except(:address).merge(user: current_user),
)
render :create_letter, status: :created
# rescue ActiveRecord::RecordInvalid => e
# render json: { error: e.record.errors.full_messages }, status: :unprocessable_entity
end
def create_instant_letter
authorize @letter_queue, policy_class: Letter::QueuePolicy
# Normalize country name using FrickinCountryNames
country = FrickinCountryNames.find_country(letter_params[:address][:country])
if country.nil?
render json: { error: "couldn't figure out country name #{letter_params[:address][:country]}" }, status: :unprocessable_entity
return
end
# Create address with normalized country
address_params = letter_params[:address].merge(country: country.alpha2)
# Normalize state name to abbreviation
address_params[:state] = FrickinCountryNames.normalize_state(country, address_params[:state])
addy = Address.new(address_params)
@letter = @letter_queue.process_letter_instantly!(
addy,
letter_params.except(:address).merge(user: current_user),
)
@expand << :label
render :create_letter, status: :created
rescue ActiveRecord::RecordNotFound
return render json: { error: "Queue not found" }, status: :not_found
rescue ActiveRecord::RecordInvalid => e
return render json: { error: e.record.errors.full_messages.join(", ") }, status: :unprocessable_entity
end
private
def set_letter_queue
@letter_queue = Letter::Queue.find_by!(slug: params[:id])
# grossest hack on the planet, nora why are you like this
raise ActiveRecord::RecordNotFound if @letter_queue.is_a?(Letter::InstantQueue)
end
def set_instant_letter_queue
@letter_queue = Letter::InstantQueue.find_by!(slug: params[:id])
end
def letter_params
params.permit(
:rubber_stamps,
:recipient_email,
:idempotency_key,
:return_address_name,
metadata: {},
address: [
:first_name,
:last_name,
:line_1,
:line_2,
:city,
:state,
:postal_code,
:country,
],
)
end
def normalize_country(country)
return "US" if country.blank? || country.downcase == "usa" || country.downcase == "united states"
country
end
end
end
end

View file

@ -0,0 +1,29 @@
module API
module V1
class LettersController < ApplicationController
before_action :set_letter, only: [:show, :mark_printed]
def show
authorize @letter
end
def by_tag
@letters = Letter.where("? = ANY(tags)", params[:tag])
authorize @letters
render :letters_collection
end
def mark_printed
authorize @letter
@letter.mark_printed!
render json: { message: "Letter marked as printed" }
end
private
def set_letter
@letter = Letter.find_by_public_id!(params[:id])
end
end
end
end

View file

@ -0,0 +1,13 @@
module API
module V1
class QZTraysController < ApplicationController
def cert
send_data QZTrayService.certificate
end
def sign
send_data QZTrayService.sign(params.require(:request))
end
end
end
end

View file

@ -0,0 +1,50 @@
module API
module V1
class TagsController < ApplicationController
# skip_before_action :authenticate!
def index
@tags = ::ApplicationController.helpers.available_tags
end
def show
@tag = params[:id]
letter_query = Letter.with_any_tags([@tag]).where(aasm_state: [:mailed, :received])
wh_order_query = Warehouse::Order.with_any_tags([@tag]).where.not(aasm_state: [:draft, :errored])
if letter_query.none? && wh_order_query.none?
render json: { error: "no letters or warehouse orders found for tag #{@tag}...?" }, status: :not_found
return
end
cache_key = "api/v1/tags/#{@tag}"
cache_options = params[:no_cache] ? { force: true } : { expires_in: 5.minutes }
cached_data = Rails.cache.fetch(cache_key, cache_options) do
{
letter_count: letter_query.count,
letter_postage_cost: letter_query.sum(:postage),
warehouse_order_count: wh_order_query.count,
warehouse_order_postage_cost: wh_order_query.sum(:postage_cost),
warehouse_order_labor_cost: wh_order_query.sum(:labor_cost),
warehouse_order_contents_cost: wh_order_query.sum(:contents_cost),
warehouse_order_total_cost: wh_order_query.sum(:postage_cost) + wh_order_query.sum(:labor_cost) + wh_order_query.sum(:contents_cost),
}
end
@letter_count = cached_data[:letter_count]
@letter_postage_cost = cached_data[:letter_postage_cost]
@warehouse_order_count = cached_data[:warehouse_order_count]
@warehouse_order_postage_cost = cached_data[:warehouse_order_postage_cost]
@warehouse_order_labor_cost = cached_data[:warehouse_order_labor_cost]
@warehouse_order_contents_cost = cached_data[:warehouse_order_contents_cost]
@warehouse_order_total_cost = cached_data[:warehouse_order_total_cost]
end
def letters
@letters = Letter.with_any_tags(params[:id])
end
end
end
end

View file

@ -0,0 +1,10 @@
module API
module V1
class UsersController < ApplicationController
def show
@user = authorize current_user
end
end
end
end

View file

@ -0,0 +1,50 @@
class APIKeysController < ApplicationController
before_action :set_api_key, except: [:index, :new, :create]
def index
authorize APIKey
@api_keys = policy_scope(APIKey)
end
def new
authorize APIKey
@api_key = APIKey.new(user: current_user)
end
def create
permitted_params = [:name, :pii]
permitted_params << :may_impersonate if current_user.admin?
@api_key = APIKey.new(params.require(:api_key).permit(*permitted_params).merge(user: current_user))
authorize @api_key
if @api_key.save
redirect_to api_key_path(@api_key)
else
flash[:error] = @api_key.errors.full_messages.to_sentence
redirect_to new_api_key_path(@api_key)
end
end
def show
authorize @api_key
end
def revoke_confirm
authorize @api_key
end
def revoke
authorize @api_key
@api_key.revoke!
flash[:success] = "terminated with extreme prejudice."
redirect_to api_key_path(@api_key)
end
private
def set_api_key
@api_key = policy_scope(APIKey).find(params[:id])
end
end

View file

@ -0,0 +1,39 @@
class ApplicationController < ActionController::Base
include Pundit::Authorization
after_action :verify_authorized
helper_method :current_user, :user_signed_in?
before_action :authenticate_user!, :set_honeybadger_context
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end
def user_signed_in?
!!current_user
end
def authenticate_user!
unless user_signed_in?
redirect_to login_path, alert: ("you need to be logged in!" unless request.env["PATH_INFO"] == "/back_office")
end
end
def set_honeybadger_context
Honeybadger.context({
user_id: current_user&.id,
user_email: current_user&.email,
})
end
rescue_from Pundit::NotAuthorizedError do |e|
flash[:error] = "you don't seem to be authorized ask nora?"
redirect_to root_path
end
rescue_from ActiveRecord::RecordNotFound do |e|
flash[:error] = "sorry, couldn't find that object... (404)"
redirect_to root_path
end
end

View file

@ -0,0 +1,111 @@
class BaseBatchesController < ApplicationController
before_action :set_batch, except: %i[ index new create ]
before_action :setup_csv_fields, only: %i[ map_fields set_mapping ]
REQUIRED_FIELDS = %w[first_name line_1 city state postal_code country].freeze
PREVIEW_ROWS = 3
# GET /batches or /batches.json
def index
authorize Batch
@batches = policy_scope(Batch).order(created_at: :desc)
end
# GET /batches/1 or /batches/1.json
def show
authorize @batch
end
# GET /batches/1/edit
def edit
authorize @batch
end
# PATCH/PUT /batches/1 or /batches/1.json
def update
authorize @batch
respond_to do |format|
if @batch.update(batch_params)
format.html { redirect_to @batch, notice: "Batch was successfully updated." }
format.json { render :show, status: :ok, location: @batch }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @batch.errors, status: :unprocessable_entity }
end
end
end
# DELETE /batches/1 or /batches/1.json
def destroy
authorize @batch
@batch.destroy!
respond_to do |format|
format.html { redirect_to batches_path, status: :see_other, notice: "Batch was successfully destroyed." }
format.json { head :no_content }
end
end
def map_fields
authorize @batch, :map_fields?
end
def set_mapping
authorize @batch, :set_mapping?
mapping = mapping_params.to_h
# Invert the mapping to get from CSV columns to address fields
inverted_mapping = mapping.invert
# Validate required fields
missing_fields = REQUIRED_FIELDS.reject { |field| inverted_mapping[field].present? }
if missing_fields.any?
flash.now[:error] = "Please map the following required fields: #{missing_fields.join(", ")}"
render :map_fields, status: :unprocessable_entity
return
end
if @batch.update!(field_mapping: inverted_mapping)
begin
@batch.run_map!
rescue StandardError => e
raise
Rails.logger.warn(e)
uuid = Honeybadger.notify(e)
redirect_to send("#{@batch.class.name.split("::").first.downcase}_batch_path", @batch), flash: { alert: "error mapping fields! #{e.message} (please report EID: #{uuid})" }
return
end
redirect_to send("process_confirm_#{@batch.class.name.split("::").first.downcase}_batch_path", @batch), notice: "Field mapping saved. Please review and process your batch."
else
flash.now[:error] = "failed to save field mapping. #{@batch.errors.full_messages.join(", ")}"
render :map_fields, status: :unprocessable_entity
end
end
private
def set_batch
@batch = Batch.find(params[:id])
end
def setup_csv_fields
csv_rows = CSV.parse(@batch.csv_data)
@csv_headers = csv_rows.first
@csv_preview = csv_rows[1..PREVIEW_ROWS] || []
# Get fields based on batch type
@address_fields = if @batch.is_a?(Letter::Batch)
# For letter batches, include address fields and rubber_stamps
(Address.column_names - ["id", "created_at", "updated_at", "batch_id"]) +
["rubber_stamps"]
else
# For other batches, just include address fields
(Address.column_names - ["id", "created_at", "updated_at"])
end
end
def mapping_params
params.require(:mapping).permit!
end
end

View file

View file

@ -0,0 +1,21 @@
module Administrate
module ArrayParameterHandler
extend ActiveSupport::Concern
included do
before_action :handle_array_parameters
end
private
def handle_array_parameters
return unless params[:warehouse_order].present?
params[:warehouse_order].each do |key, value|
if value.is_a?(String) && value.include?(",")
params[:warehouse_order][key] = value.split(",").map(&:strip)
end
end
end
end
end

View file

@ -0,0 +1,30 @@
module Public::Frameable
extend ActiveSupport::Concern
included do
layout "public/frameable"
def set_framed
@framed = request.headers["Sec-Fetch-Dest"] == "iframe"
end
before_action :set_framed
def frame_aware_redirect_to(path, **options)
if @framed
redirect_to "#{path}#{path.include?('?') ? '&' : '?'}framed=true", **options
else
redirect_to path, **options
end
end
private
def add_framed_param(path)
uri = URI.parse(path)
params = URI.decode_www_form(uri.query || '')
params << ['framed', @framed]
uri.query = URI.encode_www_form(params)
uri.to_s
end
end
end

View file

@ -0,0 +1,37 @@
class CustomsReceiptsController < ApplicationController
def index
authorize :customs_receipt
end
def generate
authorize :customs_receipt
return redirect_to customs_receipts_path, alert: "well, ya gotta search for *something*" if params[:search].blank?
receiptable = get_receiptable(params[:search])
if receiptable.nil?
flash[:error] = "couldn't find anything about #{params[:search]}... better luck next time?"
return redirect_to customs_receipts_path
end
send_data CustomsReceipt::Generate.run(receiptable), filename: "customs_receipt.pdf", type: "application/pdf", disposition: "inline"
end
private
def get_receiptable(search)
order = Warehouse::Order.where("hc_id = :search or tracking_number = :search", search:).first
return CustomsReceipt::TheseusSpecific.receiptable_from_warehouse_order(order) if order
sanitized_airtable_search = search.gsub("'", "\\'")
msr = LSV::MarketingShipmentRequest.first_where(
"OR({Airtable ID (Automation)} = '#{sanitized_airtable_search}', {WarehouseTracking Number} = '#{sanitized_airtable_search}')"
)
return CustomsReceipt::TheseusSpecific.receiptable_from_msr(msr) if msr
nil
end
end

View file

@ -0,0 +1,6 @@
module Inspect
class IndiciaController < InspectorController
MODEL = USPS::Indicium
LINKED_FIELDS = %i(letter)
end
end

View file

@ -0,0 +1,28 @@
module Inspect
class InspectorController < ApplicationController
skip_after_action :verify_authorized
before_action :set_record
before_action do
unless current_user.admin?
redirect_to root_path, alert: "you are not authorized to access this page."
end
end
def show
@linked_fields = self.class::LINKED_FIELDS.each_with_object({}) do |field, hash|
hash[field] = nil
res = @record.send(field)
next if res.blank?
hash[field] = url_for(res) if res.present?
rescue ActionController::RoutingError
end
render "inspect/show"
end
private
def set_record
@record = self.class::MODEL.find_by_public_id(params[:id]) || self.class::MODEL.find(params[:id])
end
end
end

View file

@ -0,0 +1,8 @@
module Inspect
class IVMTREventsController < InspectorController
MODEL = USPS::IVMTR::Event
LINKED_FIELDS = %i(letter)
private
end
end

View file

@ -0,0 +1,240 @@
class Letter::BatchesController < BaseBatchesController
# GET /letter/batches
def index
authorize Letter::Batch, policy_class: Letter::BatchPolicy
@batches = policy_scope(Letter::Batch, policy_scope_class: Letter::BatchPolicy::Scope).order(created_at: :desc)
end
# GET /letter/batches/new
def new
authorize Letter::Batch, policy_class: Letter::BatchPolicy
@batch = Letter::Batch.new
end
# GET /letter/batches/:id
def show
authorize @batch, policy_class: Letter::BatchPolicy
end
# GET /letter/batches/:id/edit
def edit
authorize @batch, policy_class: Letter::BatchPolicy
end
# POST /letter/batches
def create
authorize Letter::Batch, policy_class: Letter::BatchPolicy
@batch = Letter::Batch.new(batch_params.merge(user: current_user))
if @batch.save
redirect_to map_fields_letter_batch_path(@batch), notice: "Batch was successfully created."
else
render :new, status: :unprocessable_entity
end
end
# PATCH /letter/batches/:id
def update
authorize @batch, policy_class: Letter::BatchPolicy
if @batch.update(batch_params)
validate_postage_types
if @batch.errors.any?
render :edit, status: :unprocessable_entity
return
end
# Update associated letters if the batch hasn't been processed
if @batch.may_mark_processed?
@batch.letters.update_all(
height: @batch.letter_height,
width: @batch.letter_width,
weight: @batch.letter_weight,
mailing_date: @batch.letter_mailing_date,
usps_mailer_id_id: @batch.letter_mailer_id_id,
return_address_id: @batch.letter_return_address_id,
return_address_name: @batch.letter_return_address_name,
)
end
# Always update tags and user facing title on letters
@batch.letters.update_all(
tags: @batch.tags,
user_facing_title: @batch.user_facing_title,
)
redirect_to letter_batch_path(@batch), notice: "Batch was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
# DELETE /letter/batches/:id
def destroy
authorize @batch, policy_class: Letter::BatchPolicy
@batch.destroy
redirect_to letter_batches_path, notice: "Batch was successfully destroyed."
end
def process_form
authorize @batch, :process_form?, policy_class: Letter::BatchPolicy
render :process_letter
end
def process_batch
authorize @batch, :process_batch?, policy_class: Letter::BatchPolicy
@batch = Batch.find(params[:id])
if request.post?
if letter_batch_params[:letter_mailing_date].blank?
redirect_to process_letter_batch_path(@batch), alert: "Mailing date is required"
return
end
@batch.letter_mailing_date = letter_batch_params[:letter_mailing_date]
@batch.save! # Save the mailing date before processing
# Only require payment account if indicia is selected
if letter_batch_params[:us_postage_type] == "indicia" || letter_batch_params[:intl_postage_type] == "indicia"
payment_account = USPS::PaymentAccount.find_by(id: letter_batch_params[:usps_payment_account_id])
if payment_account.nil?
redirect_to process_letter_batch_path(@batch), alert: "Please select a valid payment account when using indicia"
return
end
end
begin
@batch.process!(
payment_account: payment_account,
us_postage_type: letter_batch_params[:us_postage_type],
intl_postage_type: letter_batch_params[:intl_postage_type],
template_cycle: letter_batch_params[:template_cycle].to_s.split(",").compact_blank,
user_facing_title: letter_batch_params[:user_facing_title],
include_qr_code: letter_batch_params[:include_qr_code],
)
@batch.mark_processed! if @batch.may_mark_processed?
redirect_to letter_batch_path(@batch, print_now: letter_batch_params[:print_immediately]), notice: "Batch processed successfully"
rescue => e
uuid = Honeybadger.notify(e)
redirect_to process_letter_batch_path(@batch), alert: "Failed to process batch: #{e.message} (please report EID: #{uuid})"
end
end
end
def mark_printed
authorize @batch, :mark_printed?, policy_class: Letter::BatchPolicy
if @batch.processed?
@batch.letters.each do |letter|
letter.mark_printed! if letter.may_mark_printed?
end
flash[:success] = "all letters have been marked as printed!"
redirect_to letter_batch_path(@batch)
else
flash[:alert] = "Cannot mark letters as printed. Batch must be processed."
redirect_to letter_batch_path(@batch)
end
end
def mark_mailed
authorize @batch, :mark_mailed?, policy_class: Letter::BatchPolicy
if @batch.processed?
@batch.letters.each do |letter|
letter.mark_mailed! if letter.may_mark_mailed?
end
User::UpdateTasksJob.perform_now(current_user)
redirect_to letter_batch_path(@batch), notice: "All letters have been marked as mailed."
else
redirect_to letter_batch_path(@batch), alert: "Cannot mark letters as mailed. Batch must be processed."
end
end
def update_costs
authorize @batch, :update_costs?, policy_class: Letter::BatchPolicy
# Calculate counts without saving
us_letters = @batch.letters.joins(:address).where(addresses: { country: "US" })
intl_letters = @batch.letters.joins(:address).where.not(addresses: { country: "US" })
cost_differences = @batch.postage_cost_difference(
us_postage_type: params[:us_postage_type],
intl_postage_type: params[:intl_postage_type],
)
render json: {
total_cost: @batch.postage_cost,
cost_difference: {
us: cost_differences[:us],
intl: cost_differences[:intl],
},
us_count: us_letters.count,
intl_count: intl_letters.count,
}
end
def regenerate_form
authorize @batch, :process_batch?, policy_class: Letter::BatchPolicy
render :regenerate_labels
end
def regenerate_labels
authorize @batch, :process_batch?, policy_class: Letter::BatchPolicy
@batch.regenerate_labels!(
template_cycle: letter_batch_params[:template_cycle].to_s.split(",").compact_blank,
include_qr_code: letter_batch_params[:include_qr_code],
)
redirect_to letter_batch_path(@batch), notice: "Labels regenerated successfully"
end
private
def batch_params
params.require(:letter_batch).permit(
:csv,
:letter_template_id,
:user_facing_title,
:letter_height,
:letter_width,
:letter_weight,
:letter_mailing_date,
:letter_mailer_id_id,
:letter_return_address_id,
:letter_return_address_name,
:letter_processing_category,
tags: [],
)
end
def letter_batch_params
params.require(:batch).permit(
:csv,
:letter_height,
:letter_width,
:user_facing_title,
:letter_weight,
:letter_mailing_date,
:letter_processing_category,
:letter_mailer_id_id,
:letter_return_address_id,
:letter_return_address_name,
:us_postage_type,
:intl_postage_type,
:usps_payment_account_id,
:include_qr_code,
:print_immediately,
:template_cycle,
tags: [],
)
end
def validate_postage_types
return unless @batch.letter_return_address&.us?
if @batch.us_postage_type.present? && !%w[stamps indicia].include?(@batch.us_postage_type)
@batch.errors.add(:us_postage_type, "must be either 'stamps' or 'indicia'")
end
if @batch.intl_postage_type.present? && !%w[stamps indicia].include?(@batch.intl_postage_type)
@batch.errors.add(:intl_postage_type, "must be either 'stamps' or 'indicia'")
end
end
end

View file

@ -0,0 +1,38 @@
class Letter::InstantQueuesController < Letter::QueuesController
before_action :set_letter_queue, only: %i[ show edit update destroy ]
def new
@letter_queue = Letter::InstantQueue.new
end
def show
@letters = @letter_queue.letters
end
private
def set_letter_queue
@letter_queue = Letter::InstantQueue.find_by!(slug: params[:id])
end
def letter_queue_params
params.require(:letter_instant_queue).permit(
:name,
:type,
:letter_height,
:letter_width,
:letter_weight,
:letter_processing_category,
:letter_mailer_id_id,
:letter_return_address_id,
:letter_return_address_name,
:user_facing_title,
:template,
:postage_type,
:usps_payment_account_id,
:include_qr_code,
:letter_mailing_date,
tags: [],
)
end
end

View file

@ -0,0 +1,152 @@
class Letter::QueuesController < ApplicationController
before_action :set_letter_queue, only: %i[ show edit update destroy batch ]
skip_after_action :verify_authorized
# GET /letter/queues or /letter/queues.json
def index
authorize Letter::Queue, policy_class: Letter::QueuePolicy
@letter_queues = policy_scope(Letter::Queue, policy_scope_class: Letter::QueuePolicy::Scope)
end
# GET /letter/queues/1 or /letter/queues/1.json
def show
@letters = @letter_queue.letters.queued
@batches = @letter_queue.letter_batches
end
# GET /letter/queues/new
def new
@letter_queue = Letter::Queue.new
end
# GET /letter/queues/1/edit
def edit
end
# POST /letter/queues or /letter/queues.json
def create
@letter_queue = letter_queue_class.new(letter_queue_params.merge(user: current_user))
respond_to do |format|
if @letter_queue.save
format.html { redirect_to @letter_queue, notice: "Queue was successfully created." }
format.json { render :show, status: :created, location: @letter_queue }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @letter_queue.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /letter/queues/1 or /letter/queues/1.json
def update
respond_to do |format|
if @letter_queue.update(letter_queue_params)
format.html { redirect_to @letter_queue, notice: "Queue was successfully updated." }
format.json { render :show, status: :ok, location: @letter_queue }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @letter_queue.errors, status: :unprocessable_entity }
end
end
end
# DELETE /letter/queues/1 or /letter/queues/1.json
def destroy
@letter_queue.destroy!
respond_to do |format|
format.html { redirect_to letter_queues_path, status: :see_other, notice: "Queue was successfully destroyed." }
format.json { head :no_content }
end
end
def batch
authorize @letter_queue
unless @letter_queue.letters.any?
flash[:error] = "no letters?"
redirect_to @letter_queue
end
batch = @letter_queue.make_batch(user: current_user)
User::UpdateTasksJob.perform_now(current_user)
flash[:success] = "now do something with it!"
redirect_to process_letter_batch_path(batch, uft: @letter_queue.user_facing_title, template: @letter_queue.template)
end
def mark_printed_instants_mailed
authorize Letter::Queue
# Find all letters with "printed" status in any instant letter queue
printed_letters = Letter.joins(:queue)
.where(letter_queues: { type: "Letter::InstantQueue" })
.where(aasm_state: "printed")
if printed_letters.empty?
flash[:notice] = "No printed letters found in instant queues."
redirect_to letter_queues_path
return
end
# Mark all printed letters as mailed
marked_count = 0
failed_letters = []
printed_letters.each do |letter|
begin
letter.mark_mailed!
marked_count += 1
rescue => e
failed_letters << "#{letter.public_id} (#{e.message})"
end
end
# Update user tasks after marking letters as mailed
User::UpdateTasksJob.perform_now(current_user) if marked_count > 0
if failed_letters.any?
flash[:alert] = "Marked #{marked_count} letters as mailed, but failed to mark #{failed_letters.count} letters: #{failed_letters.join(", ")}"
else
flash[:success] = "Successfully marked #{marked_count} letters as mailed from instant queues."
end
redirect_to letter_queues_path
end
private
# Use callbacks to share common setup or constraints between actions.
def set_letter_queue
@letter_queue = Letter::Queue.find_by!(slug: params[:id])
end
def letter_queue_class
type = params[:letter_queue]&.dig(:type) || params[:letter_instant_queue]&.dig(:type)
case type
when "Letter::InstantQueue"
Letter::InstantQueue
else
Letter::Queue
end
end
# Only allow a list of trusted parameters through.
def letter_queue_params
params.require(:letter_queue).permit(
:name,
:type,
:letter_height,
:letter_width,
:letter_weight,
:letter_processing_category,
:letter_mailer_id_id,
:letter_return_address_id,
:letter_return_address_name,
:user_facing_title,
:template,
:postage_type,
:usps_payment_account_id,
:include_qr_code,
:letter_mailing_date,
tags: [],
)
end
end

View file

@ -0,0 +1,243 @@
class LettersController < ApplicationController
before_action :set_letter, except: %i[ index new create ]
# GET /letters
def index
authorize Letter
# Get all letters with their associations using policy scope
@all_letters = policy_scope(Letter).includes(:batch, :address, :usps_mailer_id, :label_attachment, :label_blob)
.where.not(aasm_state: "queued")
.order(created_at: :desc)
# Get unbatched letters with pagination
@unbatched_letters = @all_letters.not_in_batch.page(params[:page]).per(20)
# Get batched letters grouped by batch
@batched_letters = @all_letters.in_batch.group_by(&:batch)
end
# GET /letters/1
def show
authorize @letter
@available_templates = SnailMail::Service.available_templates
end
# GET /letters/new
def new
authorize Letter
@letter = Letter.new
@letter.return_address = current_user.home_return_address || ReturnAddress.first
@letter.build_address
end
# GET /letters/1/edit
def edit
authorize @letter
# If letter doesn't have a return address already, don't build one
# Let the user select one from the dropdown
end
# POST /letters
def create
@letter = Letter.new(letter_params.merge(user: current_user))
authorize @letter
# Set postage type to international_origin if return address is not US
if @letter.return_address && @letter.return_address.country != "US"
@letter.postage_type = "international_origin"
end
if @letter.save
redirect_to @letter, notice: "Letter was successfully created."
else
render :new, status: :unprocessable_entity
end
end
# PATCH/PUT /letters/1
def update
authorize @letter
if @letter.batch_id.present? && params[:letter][:postage_type].present?
redirect_to @letter, alert: "Cannot change postage type for a letter that is part of a batch."
return
end
# Set postage type to international_origin if return address is not US
if params[:letter][:return_address_id].present?
return_address = ReturnAddress.find(params[:letter][:return_address_id])
if return_address.country != "US"
params[:letter][:postage_type] = "international_origin"
end
end
if @letter.update(letter_params)
redirect_to @letter, notice: "Letter was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
# DELETE /letters/1
def destroy
authorize @letter
@letter.destroy!
redirect_to letters_path, status: :see_other, notice: "Letter was successfully destroyed."
end
# POST /letters/1/generate_label
def generate_label
authorize @letter, :generate_label?
template = params[:template]
include_qr_code = params[:qr].present?
# Generate label with specified template
begin
# Let the model method handle saving itself
if @letter.generate_label(template:, include_qr_code:)
if @letter.label.attached?
# Redirect back to the letter page with a success message
redirect_to @letter, notice: "Label was successfully generated."
else
redirect_to @letter, alert: "Failed to generate label."
end
else
redirect_to @letter, alert: "Failed to generate label: #{@letter.errors.full_messages.join(", ")}"
end
rescue => e
raise
redirect_to @letter, alert: "Error generating label: #{e.message}"
end
end
def preview_template
authorize @letter, :preview_template?
template = params["template"]
include_qr_code = params["qr"].present?
send_data SnailMail::Service.generate_label(@letter, { template:, include_qr_code: }).render, type: "application/pdf", disposition: "inline"
end
# POST /letters/1/mark_printed
def mark_printed
authorize @letter, :mark_printed?
if @letter.mark_printed!
redirect_to @letter, notice: "Letter has been marked as printed."
else
redirect_to @letter, alert: "Could not mark letter as printed: #{@letter.errors.full_messages.join(", ")}"
end
end
# POST /letters/1/mark_mailed
def mark_mailed
authorize @letter, :mark_mailed?
if @letter.mark_mailed!
User::UpdateTasksJob.perform_now(current_user)
redirect_to @letter, notice: "Letter has been marked as mailed."
else
redirect_to @letter, alert: "Could not mark letter as mailed: #{@letter.errors.full_messages.join(", ")}"
end
end
# POST /letters/1/mark_received
def mark_received
authorize @letter, :mark_received?
if @letter.mark_received!
redirect_to @letter, notice: "Letter has been marked as received."
else
redirect_to @letter, alert: "Could not mark letter as received: #{@letter.errors.full_messages.join(", ")}"
end
end
# POST /letters/1/clear_label
def clear_label
authorize @letter, :clear_label?
if @letter.pending? && @letter.label.attached?
@letter.label.purge
redirect_to @letter, notice: "Label has been cleared."
else
redirect_to @letter, alert: "Cannot clear label: Letter is not in pending state or has no label attached."
end
end
# POST /letters/1/buy_indicia
def buy_indicia
authorize @letter, :buy_indicia?
if @letter.batch_id.present?
redirect_to @letter, alert: "Cannot buy indicia for a letter that is part of a batch."
return
end
if @letter.postage_type != "indicia"
redirect_to @letter, alert: "Letter must be set to indicia postage type first."
return
end
if @letter.usps_indicium.present?
redirect_to @letter, alert: "Indicia already purchased for this letter."
return
end
payment_account = USPS::PaymentAccount.find_by(id: params[:usps_payment_account_id])
if payment_account.nil?
redirect_to @letter, alert: "Please select a valid payment account."
return
end
indicium = USPS::Indicium.new(letter: @letter, payment_account: payment_account)
begin
indicium.buy!
redirect_to @letter, notice: "Indicia purchased successfully."
rescue => e
redirect_to @letter, alert: "Failed to purchase indicia: #{e.message}"
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_letter
@letter = Letter.find_by_public_id!(params[:id])
end
# Only allow a list of trusted parameters through.
def letter_params
params.require(:letter).permit(
:body,
:height,
:width,
:weight,
:non_machinable,
:processing_category,
:postage_type,
:mailing_date,
:rubber_stamps,
:user_facing_title,
:usps_mailer_id_id,
:return_address_id,
:return_address_name,
:recipient_email,
address_attributes: [
:id,
:first_name,
:last_name,
:line_1,
:line_2,
:city,
:state,
:postal_code,
:country,
],
return_address_attributes: [
:id,
:name,
:line_1,
:line_2,
:city,
:state,
:postal_code,
:country,
],
tags: [],
)
end
end

View file

@ -0,0 +1,39 @@
module Public
module API
module V1
class ApplicationController < ActionController::API
prepend_view_path "app/views/public/api/v1"
attr_reader :current_public_user
before_action :authenticate!
before_action :set_expand
include ActionController::HttpAuthentication::Token::ControllerMethods
rescue_from Pundit::NotAuthorizedError do |e|
render json: { error: "not_authorized" }, status: :forbidden
end
rescue_from ActiveRecord::RecordNotFound do |e|
render json: { error: "resource_not_found", message: ("Couldn't locate that #{e.model.constantize.model_name.human}." if e.model) }.compact_blank, status: :not_found
end
private
def set_expand
@expand = params[:expand].to_s.split(",").map { |e| e.strip.to_sym }
end
def authenticate!
@current_token = authenticate_with_http_token { |t, _options| Public::APIKey.find_by(token: t) }
unless @current_token&.active?
return render json: { error: "invalid_auth" }, status: :unauthorized
end
@current_public_user = @current_token.public_user
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Public
module API
module V1
class LettersController < ApplicationController
def index
@letters = Letter.where(recipient_email: current_public_user.email)
end
def show
@letter = Letter.where(recipient_email: current_public_user.email).find_by_public_id!(params[:id])
end
end
end
end
end

View file

@ -0,0 +1,19 @@
module Public
module API
module V1
class LSVController < ApplicationController
def index
@lsv = LSV::TYPES.map { |type| type.find_by_email(current_public_user.email) }.flatten
render :index
end
def show
@lsv = LSV::SLUGS[params[:slug].to_sym]&.find(params[:id])
raise ActiveRecord::RecordNotFound unless @lsv && @lsv.email == current_public_user&.email
rescue Norairrecord::RecordNotFoundError
raise ActiveRecord::RecordNotFound
end
end
end
end
end

View file

@ -0,0 +1,18 @@
module Public
module API
module V1
class MailController < ApplicationController
def index
@mail =
Warehouse::Order.where(recipient_email: current_public_user.email) +
Letter.where(recipient_email: current_public_user.email)
unless params[:no_load_lsv]
@mail += LSV::TYPES.map { |type| type.find_by_email(current_public_user.email) }.flatten
end
@mail.sort_by!(&:created_at).reverse!
render :index
end
end
end
end
end

View file

@ -0,0 +1,22 @@
module Public
module API
module V1
class PackagesController < ApplicationController
before_action :set_package, only: [:show]
def index
@packages = Warehouse::Order.where(recipient_email: current_public_user.email)
end
def show
end
private
def set_package
@package = Warehouse::Order.find_by!(hc_id: params[:id])
end
end
end
end
end

View file

@ -0,0 +1,12 @@
module Public
module API
module V1
class UsersController < ApplicationController
def me
@user = current_public_user
render :show
end
end
end
end
end

View file

@ -0,0 +1,43 @@
module Public
class APIKeysController < ApplicationController
before_action :set_api_key, except: [:index, :new, :create]
before_action :authenticate_public_user!
def index
@api_keys = APIKey.where(public_user: current_public_user)
end
def new
@api_key = APIKey.new(public_user: current_public_user)
end
def create
@api_key = APIKey.new(params.require(:public_api_key).permit(:name).merge(public_user: current_public_user))
if @api_key.save
redirect_to public_api_key_path(@api_key)
else
flash[:error] = @api_key.errors.full_messages.to_sentence
redirect_to new_public_api_key_path
end
end
def show
end
def revoke_confirm
end
def revoke
@api_key.revoke!
flash[:success] = "terminated with extreme prejudice."
redirect_to public_api_key_path(@api_key)
end
private
def set_api_key
@api_key = APIKey.where(public_user: current_public_user).find(params[:id])
end
end
end

View file

@ -0,0 +1,52 @@
module Public
class ApplicationController < ActionController::Base
include Pundit::Authorization
layout "public"
before_action do
Honeybadger.context({
user_id: current_public_user&.id,
user_email: current_public_user&.email,
real_user_id: current_user&.id,
real_user_email: current_user&.email,
impersonator_user_id: session[:public_impersonator_user_id],
})
end
helper_method :current_user, :current_public_user, :public_user_signed_in?, :authenticate_public_user!, :impersonating?
# DO NOT USE (in most cases :-P)
def current_user
@current_user ||= ::User.find_by(id: session[:user_id]) if session[:user_id]
end
def current_public_user
@current_public_user ||= Public::User.find_by(id: session[:public_user_id]) if session[:public_user_id]
end
def public_user_signed_in?
!!current_public_user
end
def authenticate_public_user!
unless public_user_signed_in?
redirect_to public_login_path, alert: ("you need to be logged in!" unless request.env["PATH_INFO"] == "/")
end
end
def impersonating?
!!session[:public_impersonator_user_id]
end
rescue_from Pundit::NotAuthorizedError do |e|
flash[:error] = "hey, you can't do that!"
redirect_to public_root_path
end
rescue_from ActiveRecord::RecordNotFound do |e|
flash[:error] = "sorry, couldn't find that page!"
redirect_to public_root_path
end
end
end

View file

@ -0,0 +1,33 @@
module Public
class ImpersonationsController < ApplicationController
def new
authorize Impersonation
@impersonation = Impersonation.new
end
def create
@impersonation = Impersonation.new(impersonation_params.merge(user: current_user))
authorize @impersonation
if @impersonation.save
public_user = Public::User.find_or_create_by!(email: impersonation_params[:target_email])
session[:public_user_id] = public_user.id
session[:public_impersonator_user_id] = current_user.id
redirect_to public_root_path
else
render :new
end
end
def stop_impersonating
session[:public_user_id] = nil
session[:public_impersonator_user_id] = nil
redirect_to public_root_path
end
def impersonation_params
params.require(:public_impersonation).permit(:target_email, :justification)
end
end
end

View file

@ -0,0 +1,49 @@
module Public
class LeaderboardsController < ApplicationController
layout 'public/frameable'
before_action :set_framed
def this_week
@tab = :this_week
@lb = fetch_leaderboard(:week, Time.current.beginning_of_week)
render :show
end
def this_month
@tab = :this_month
@lb = fetch_leaderboard(:month, Time.current.beginning_of_month)
render :show
end
def all_time
@tab = :all_time
@lb = fetch_leaderboard(:all_time)
render :show
end
private
def set_framed
@framed = true
end
def fetch_leaderboard(period, start_time = nil)
cache_key = "letter_leaderboard/#{period}"
cache_key += "/#{start_time.to_i}" if start_time
Rails.cache.fetch(cache_key, expires_in: 10.minutes) do
query = ::User.joins(:letters)
.where(letters: { aasm_state: ['mailed', 'received'] })
.group('users.id')
.select('users.*, COUNT(letters.id) as letter_count')
.having('COUNT(letters.id) > 0')
.order('letter_count DESC')
.limit(100)
query = query.where('letters.mailed_at >= ?', start_time) if start_time
query
end
end
end
end

View file

@ -0,0 +1,41 @@
module Public
class LettersController < ApplicationController
include Frameable
before_action :set_letter
def show
@framed = params[:framed].present? ? params[:framed] == 'true' : request.headers["Sec-Fetch-Dest"] == "iframe"
render "public/letters/show"
end
def mark_received
@framed = params[:framed]
if @letter.may_mark_received?
@letter.mark_received!
@received = true
frame_aware_redirect_to public_letter_path(@letter)
else
flash[:alert] = "huh?"
return frame_aware_redirect_to public_letter_path(@letter)
end
end
def mark_mailed
if @letter.may_mark_mailed?
@letter.mark_mailed!
frame_aware_redirect_to public_letter_path(@letter)
else
flash[:alert] = "huh?"
return frame_aware_redirect_to public_letter_path(@letter)
end
end
private
def set_letter
@letter = Letter.find_by_public_id!(params[:id])
@events = @letter.events
end
end
end

View file

@ -0,0 +1,31 @@
module Public
class LSVController < ApplicationController
include Frameable
def show
@lsv = LSV::SLUGS[params[:slug].to_sym]&.find(params[:id])
raise ActiveRecord::RecordNotFound unless @lsv && @lsv.email == current_public_user&.email
rescue Norairrecord::RecordNotFoundError
raise ActiveRecord::RecordNotFound
end
def customs_receipt
@msr = LSV::MarketingShipmentRequest.find(params[:id])
raise ActiveRecord::RecordNotFound unless @msr && @msr.email == current_public_user&.email && @msr.country != "US"
rescue Norairrecord::RecordNotFoundError
raise ActiveRecord::RecordNotFound
end
def generate_customs_receipt
@msr = LSV::MarketingShipmentRequest.find(params[:id])
raise ActiveRecord::RecordNotFound unless @msr && @msr.email == current_public_user&.email && @msr.country != "US"
CustomsReceipt::MSRReceiptJob.perform_later(@msr.id)
flash[:success] = "check your email in a little bit!"
return redirect_to show_lsv_path(slug: "msr", id: @msr.id)
rescue Norairrecord::RecordNotFoundError
raise ActiveRecord::RecordNotFound
end
end
end

View file

@ -0,0 +1,15 @@
module Public
class MailController < ApplicationController
before_action :authenticate_public_user!
def index
@mail =
Warehouse::Order.where(recipient_email: current_public_user.email) +
Letter.where(recipient_email: current_public_user.email)
unless params[:no_load_lsv]
@mail += LSV::TYPES.map { |type| type.find_by_email(current_public_user.email) }.flatten
end
@mail.sort_by!(&:created_at).reverse!
end
end
end

View file

@ -0,0 +1,16 @@
require "set"
module Public
class MapsController < ApplicationController
include Frameable
layout "public/frameable"
def show
@return_path = public_root_path
@letters_data = Rails.cache.fetch("map_data") do
# If cache is empty, run the job synchronously as a fallback
Public::UpdateMapDataJob.perform_now
end
end
end
end

View file

@ -0,0 +1,36 @@
module Public
class PackagesController < ApplicationController
include Frameable
before_action :set_package
def show
if @package.is_a? Warehouse::Order
render "public/warehouse/orders/show"
end
end
def customs_receipt
raise ActiveRecord::RecordNotFound unless @package.is_a?(Warehouse::Order) &&
@package.recipient_email == current_public_user&.email &&
!@package.address.us?
end
def generate_customs_receipt
raise ActiveRecord::RecordNotFound unless @package.is_a?(Warehouse::Order) &&
@package.recipient_email == current_public_user&.email &&
!@package.address.us?
# Queue the job to generate and send the customs receipt
CustomsReceipt::WarehouseOrderReceiptJob.perform_later(@package.id)
flash[:success] = "check your email in a little bit!"
redirect_to public_package_path(@package)
end
private
def set_package
@package = Warehouse::Order.find_by!(hc_id: params[:id])
end
end
end

View file

@ -0,0 +1,21 @@
module Public
class PublicIdentifiableController < ApplicationController
def show
prefix = params[:public_id].split("!").first&.downcase
case prefix
when "ltr"
@record = Letter.find_by_public_id!(params[:public_id])
redirect_to public_letter_path(@record)
when "pkg"
@record = Warehouse::Order.find_by_public_id!(params[:public_id])
redirect_to public_package_path(@record)
else
raise ActiveRecord::RecordNotFound, "no record found with public_id: #{params[:public_id]}"
end
rescue ActiveRecord::RecordNotFound => e
flash[:alert] = "what are you even looking for..?"
redirect_to public_root_path
end
end
end

View file

@ -0,0 +1,50 @@
module Public
class SessionsController < ApplicationController
def send_email
begin
@email = params.require(:email)
rescue ActionController::ParameterMissing => e
@error = "you do need to enter an email address...."
return render "public/static_pages/login"
end
if @email.ends_with?("hack.af")
@error = "come on, is that your <b>real</b> email? say hi to orpheus for me".html_safe
return render "public/static_pages/login"
end
address = ValidEmail2::Address.new(@email)
unless address.valid?
@error = "that isn't shaped like an email..."
return render "public/static_pages/login"
end
if Rails.env.production?
SendLoginEmailJob.perform_later(@email)
else
SendLoginEmailJob.perform_now(@email)
end
end
def login_code
valid_code = LoginCode.where(token: params[:token], used_at: nil)
.where("expires_at > ?", Time.current)
.first
if valid_code
valid_code.mark_used!
session[:public_user_id] = valid_code.user_id
redirect_to public_root_path, notice: "you're in!"
else
redirect_to public_root_path, alert: "invalid or expired sign-in link...?"
end
end
def destroy
session[:public_user_id] = nil
session[:public_impersonator_user_id] = nil
redirect_to public_root_path, notice: "signed out!"
end
end
end

View file

@ -0,0 +1,10 @@
module Public
class StaticPagesController < ApplicationController
def root
# flash[:alert] = "bruh"
end
def login
end
end
end

View file

@ -0,0 +1,61 @@
class PublicIdsController < ApplicationController
# GET /id/:public_id
#
skip_after_action :verify_authorized
#
def index
end
def lookup
# The public_id contains the prefix that determines the model class
prefix = params[:id].split("!").first&.downcase
id_part = params[:id].split("!").last
# Find the corresponding model based on the prefix
case prefix
when "mtr"
@record = USPS::IVMTR::Event.find_by_public_id!(params[:id])
@letter = @record.letter
if current_user.admin?
redirect_to inspect_iv_mtr_event_path(@record)
else
if @letter.present?
redirect_to public_letter_path(@letter)
else
redirect_to public_ids_path, alert: "MTR event found, but no associated letter...?"
end
end
when "hackapost", "dev"
@indicium = USPS::Indicium.find(id_part[1...])
@letter = @indicium.letter
if current_user.admin?
redirect_to inspect_indicium_path(@indicium)
else
if @letter.present?
redirect_to public_letter_path(@letter)
else
redirect_to public_ids_path, alert: "indicium found, but no associated letter...?"
end
end
else
# bad hack:
clazzes = ActiveRecord::Base.descendants.select { |c| c.included_modules.include?(PublicIdentifiable) }
clazz = clazzes.find { |c| c.public_id_prefix == prefix }
unless clazz.present?
return redirect_to public_ids_path, alert: "don't think we have that prefix: #{prefix}"
end
@record = clazz.find_by_public_id(params[:id])
unless @record.present?
return redirect_to public_ids_path, alert: "no #{clazz.name} found with public id #{params[:id]}"
end
redirect_to url_for(@record)
# If no matching prefix is found, return 404
# raise ActiveRecord::RecordNotFound, "No record found with public_id: #{params[:id]}"
end
rescue ActiveRecord::RecordNotFound => e
flash[:alert] = "Record not found"
redirect_to public_ids_path
end
end

View file

@ -0,0 +1,19 @@
class QZTraysController < ApplicationController
skip_after_action :verify_authorized
skip_before_action :verify_authenticity_token, only: [:sign]
skip_before_action :authenticate_user!, only: [:test_print]
def cert
send_data QZTrayService.certificate
end
def settings
end
def sign
send_data QZTrayService.sign(params.require(:request))
end
def test_print
send_file(Rails.root.join('app', 'lib', 'test_print.pdf'), type: 'application/pdf', disposition: 'inline')
end
end

View file

@ -0,0 +1,82 @@
class ReturnAddressesController < ApplicationController
before_action :set_return_address, only: [:edit, :update, :destroy]
def index
authorize ReturnAddress
@return_addresses = ReturnAddress.where(shared: true).or(ReturnAddress.where(user: current_user))
end
def new
authorize ReturnAddress
@return_address = ReturnAddress.new
@return_address.user = current_user if user_signed_in?
end
def edit
authorize @return_address
end
def create
@return_address = ReturnAddress.new(return_address_params)
@return_address.user = current_user if user_signed_in?
authorize @return_address
if @return_address.save
# If this was created from the letter form, redirect back to the letter
if params[:from_letter].present?
redirect_to new_letter_path, notice: "Return address was successfully created. Please select it from the dropdown."
else
flash[:success] = "Return address was successfully created."
redirect_to return_addresses_path
end
else
render :new, status: :unprocessable_entity
end
end
def update
authorize @return_address
if @return_address.update(return_address_params)
# If this was updated from the letter form, redirect back to the letter
if params[:from_letter].present?
redirect_to new_letter_path, notice: "Return address was successfully updated. Please select it from the dropdown."
else
redirect_to @return_address, notice: "Return address was successfully updated."
end
else
render :edit, status: :unprocessable_entity
end
end
def destroy
authorize @return_address
if @return_address.letters.any?
redirect_to return_addresses_url, alert: "return address has letters associated with it, so it can't be deleted :-("
else
@return_address.destroy
redirect_to return_addresses_url, notice: "Return address was successfully destroyed."
end
end
def set_as_home
@return_address = ReturnAddress.find(params[:id])
authorize @return_address
current_user.update!(home_return_address: @return_address)
flash[:success] = "#{@return_address.display_name} is now your default return address."
redirect_to return_addresses_url
end
private
def set_return_address
@return_address = ReturnAddress.find(params[:id])
end
def return_address_params
params.require(:return_address).permit(:name, :line_1, :line_2, :city, :state, :postal_code, :country, :shared, :user_id, :from_letter)
end
end

View file

@ -0,0 +1,67 @@
class SessionsController < ApplicationController
skip_before_action :authenticate_user!, only: [:new, :create]
skip_after_action :verify_authorized
def new
redirect_uri = url_for(action: :create, only_path: false)
Rails.logger.info "Starting Slack OAuth flow with redirect URI: #{redirect_uri}"
redirect_to User.authorize_url(redirect_uri),
host: "https://slack.com",
allow_other_host: true
end
def create
redirect_uri = url_for(action: :create, only_path: false)
if params[:error].present?
Rails.logger.error "Slack OAuth error: #{params[:error]}"
uuid = Honeybadger.notify("Slack OAuth error: #{params[:error]}")
redirect_to login_path, alert: "failed to authenticate with Slack! (error: #{uuid})"
return
end
begin
@user = User.from_slack_token(params[:code], redirect_uri)
rescue => e
Rails.logger.error "Error creating user from Slack data: #{e.message}"
uuid = Honeybadger.notify(e)
redirect_to login_path, alert: "error authenticating! (error: #{uuid})"
return
end
if @user&.persisted?
session[:user_id] = @user.id
flash[:success] = "welcome aboard!"
redirect_to root_path
else
Rails.logger.error "Failed to create/update user from Slack data"
redirect_to login_path, alert: "are you sure you should be here?"
end
end
def impersonate
unless current_user.admin?
redirect_to root_path, alert: "you are not authorized to impersonate users. this incident has been reported :-P"
Honeybadger.notify("Impersonation attempt by #{current_user.username} to #{params[:id]}")
return
end
session[:impersonator_user_id] ||= current_user.id
user = User.find(params[:id])
session[:user_id] = user.id
flash[:success] = "hey #{user.username}! how's it going? nice 'stache and glasses!"
redirect_to root_path
end
def stop_impersonating
session[:user_id] = session[:impersonator_user_id]
session[:impersonator_user_id] = nil
redirect_to root_path, notice: "welcome back, 007!"
end
def destroy
session[:user_id] = nil
redirect_to root_path, notice: "bye, see you next time!"
end
end

View file

@ -0,0 +1,70 @@
class SourceTagsController < ApplicationController
before_action :set_source_tag, only: %i[ show edit update destroy ]
# GET /source_tags or /source_tags.json
def index
@source_tags = SourceTag.all
end
# GET /source_tags/1 or /source_tags/1.json
def show
end
# GET /source_tags/new
def new
@source_tag = SourceTag.new
end
# GET /source_tags/1/edit
def edit
end
# POST /source_tags or /source_tags.json
def create
@source_tag = SourceTag.new(source_tag_params)
respond_to do |format|
if @source_tag.save
format.html { redirect_to @source_tag, notice: "Source tag was successfully created." }
format.json { render :show, status: :created, location: @source_tag }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @source_tag.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /source_tags/1 or /source_tags/1.json
def update
respond_to do |format|
if @source_tag.update(source_tag_params)
format.html { redirect_to @source_tag, notice: "Source tag was successfully updated." }
format.json { render :show, status: :ok, location: @source_tag }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @source_tag.errors, status: :unprocessable_entity }
end
end
end
# DELETE /source_tags/1 or /source_tags/1.json
def destroy
@source_tag.destroy!
respond_to do |format|
format.html { redirect_to source_tags_path, status: :see_other, notice: "Source tag was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_source_tag
@source_tag = SourceTag.find(params.expect(:id))
end
# Only allow a list of trusted parameters through.
def source_tag_params
params.expect(source_tag: [ :slug, :name, :owner ])
end
end

View file

@ -0,0 +1,11 @@
class StaticPagesController < ApplicationController
skip_before_action :authenticate_user!, only: [:login]
skip_after_action :verify_authorized
def index
end
def login
render :login, layout: false
end
end

View file

@ -0,0 +1,71 @@
class TagsController < ApplicationController
skip_after_action :verify_authorized
# GET /tags or /tags.json
def index
@common_tags = CommonTag.all
@tags = Rails.cache.fetch "tags_list" do
warehouse_order_tags = Warehouse::Order.all_tags
letter_tags = Letter.all_tags
(warehouse_order_tags + letter_tags).uniq.compact_blank - @common_tags.map(&:tag)
end
end
# GET /tags/1 or /tags/1.json
def show
tag = params[:id]
time_period = params[:time_period] || "all_time"
year = params[:year]&.to_i || Time.current.year
month = params[:month]&.to_i
# Base queries
letter_query = Letter.with_any_tags([tag]).where.not(aasm_state: "queued")
wh_order_query = Warehouse::Order.with_any_tags([tag])
# Apply time period filter
case time_period
when "ytd"
start_date = Date.new(year, 1, 1)
letter_query = letter_query.where("created_at >= ?", start_date)
wh_order_query = wh_order_query.where("created_at >= ?", start_date)
when "month"
if month.present?
start_date = Date.new(year, month, 1)
end_date = start_date.end_of_month
letter_query = letter_query.where(created_at: start_date..end_date)
wh_order_query = wh_order_query.where(created_at: start_date..end_date)
end
when "last_week"
letter_query = letter_query.where("created_at >= ?", 1.week.ago)
wh_order_query = wh_order_query.where("created_at >= ?", 1.week.ago)
when "last_month"
letter_query = letter_query.where("created_at >= ?", 1.month.ago)
wh_order_query = wh_order_query.where("created_at >= ?", 1.month.ago)
when "last_year"
letter_query = letter_query.where("created_at >= ?", 1.year.ago)
wh_order_query = wh_order_query.where("created_at >= ?", 1.year.ago)
end
@letter_count = letter_query.count
@letter_postage_cost = letter_query.sum(:postage)
@warehouse_order_count = wh_order_query.count
@warehouse_order_postage_cost = wh_order_query.sum(:postage_cost)
@warehouse_order_labor_cost = wh_order_query.sum(:labor_cost)
@warehouse_order_contents_cost = wh_order_query.sum(:contents_cost)
@warehouse_order_total_cost = @warehouse_order_postage_cost + @warehouse_order_labor_cost + @warehouse_order_contents_cost
@tag = tag
@time_period = time_period
@year = year
@month = month
@years = (2020..Time.current.year).to_a.reverse
@months = (1..12).map { |m| [Date::MONTHNAMES[m], m] }
end
def refresh
Rails.cache.delete("tags_list")
flash[:success] = "refreshed!"
redirect_to tags_path
end
end

View file

@ -0,0 +1,22 @@
class TasksController < ApplicationController
skip_after_action :verify_authorized
before_action :find_tasks
def badge
render :badge, layout: false
end
def show
render :show
end
def refresh
User::UpdateTasksJob.perform_now(current_user)
redirect_to tasks_path
end
def find_tasks
@tasks = Rails.cache.read("user_tasks/#{current_user.id}")
@tasks ||= User::UpdateTasksJob.perform_now(current_user)
end
end

View file

@ -0,0 +1,70 @@
class UsersController < ApplicationController
before_action :set_user, only: %i[ show edit update destroy ]
# GET /users or /users.json
def index
@users = User.all
end
# GET /users/1 or /users/1.json
def show
end
# GET /users/new
def new
@user = User.new
end
# GET /users/1/edit
def edit
end
# POST /users or /users.json
def create
@user = User.new(user_params)
respond_to do |format|
if @user.save
format.html { redirect_to @user, notice: "User was successfully created." }
format.json { render :show, status: :created, location: @user }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /users/1 or /users/1.json
def update
respond_to do |format|
if @user.update(user_params)
format.html { redirect_to @user, notice: "User was successfully updated." }
format.json { render :show, status: :ok, location: @user }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
# DELETE /users/1 or /users/1.json
def destroy
@user.destroy!
respond_to do |format|
format.html { redirect_to users_path, status: :see_other, notice: "User was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
@user = User.find(params.expect(:id))
end
# Only allow a list of trusted parameters through.
def user_params
params.expect(user: [ :slack_id, :email, :is_admin ])
end
end

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
module USPS
module IVMTR
class WebhookController < ActionController::Base
skip_before_action :verify_authenticity_token
before_action do
http_basic_authenticate_or_request_with(
name: "my_best_friend_the_informed_visibility_robot",
password: Rails.application.credentials.dig(:usps, :iv_mtr, :webhook_password),
realm: "IV-MTR",
message: "nice try, jackwagon"
)
end
def ingest
data = JSON.parse(request.raw_post)
batch = USPS::IVMTR::RawJSONBatch.create(
payload: data["events"],
message_group_id: data["msgGrpId"],
processed: false
)
USPS::IVMTR::ImportEventsJob.perform_later(batch)
render json: {message: "hey, thanks!"}
end
end
end
end

View file

@ -0,0 +1,137 @@
class Warehouse::BatchesController < BaseBatchesController
before_action :set_allowed_templates, only: %i[ new create ]
# GET /warehouse/batches or /warehouse/batches.json
def index
authorize Warehouse::Batch
@batches = policy_scope(Warehouse::Batch).order(created_at: :desc)
end
# GET /warehouse/batches/1 or /warehouse/batches/1.json
def show
authorize @batch
end
# GET /warehouse/batches/new
def new
authorize Warehouse::Batch
@batch = Warehouse::Batch.new
end
# GET /warehouse/batches/1/edit
def edit
authorize @batch
end
# POST /warehouse/batches or /warehouse/batches.json
def create
authorize Warehouse::Batch
@batch = Warehouse::Batch.new(batch_params.merge(user: current_user))
if @batch.save
@batch.aasm_state = :awaiting_field_mapping
@batch.save!
redirect_to map_fields_warehouse_batch_path(@batch), notice: "Please map your CSV fields to address fields."
else
render :new, status: :unprocessable_entity
end
end
# PATCH/PUT /warehouse/batches/1 or /warehouse/batches/1.json
def update
authorize @batch
if @batch.update(batch_params)
# If template changed and batch hasn't been processed, recreate orders
if @batch.may_mark_processed? && @batch.saved_change_to_warehouse_template_id?
# Delete existing orders
@batch.orders.destroy_all
# Recreate orders from addresses with new template
@batch.addresses.each do |address|
Warehouse::Order.from_template(
@batch.warehouse_template,
batch: @batch,
recipient_email: address.email,
address: address,
user: @batch.user,
idempotency_key: "batch_#{@batch.id}_address_#{address.id}",
user_facing_title: @batch.warehouse_user_facing_title,
tags: @batch.tags,
).save!
end
end
# Always update tags and user facing title on orders
@batch.orders.update_all(
tags: @batch.tags,
user_facing_title: @batch.warehouse_user_facing_title,
)
redirect_to warehouse_batch_path(@batch), notice: "Batch was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
authorize @batch
@batch.destroy
redirect_to warehouse_batches_path, notice: "Batch was successfully destroyed."
end
def process_form
authorize @batch, :process_form?
render :process_warehouse
end
def process_batch
authorize @batch, :process_batch?
if @batch.process!
redirect_to warehouse_batch_path(@batch), notice: "Batch was successfully processed."
else
render :process_form, status: :unprocessable_entity
end
end
def set_mapping
authorize @batch, :set_mapping?
mapping = mapping_params.to_h
# Invert the mapping to get from CSV columns to address fields
inverted_mapping = mapping.invert
# Validate required fields
missing_fields = REQUIRED_FIELDS.reject { |field| inverted_mapping[field].present? }
if missing_fields.any?
flash.now[:error] = "Please map the following required fields: #{missing_fields.join(", ")}"
render :map_fields, status: :unprocessable_entity
return
end
if @batch.update!(field_mapping: inverted_mapping)
begin
@batch.run_map!
rescue StandardError => e
Rails.logger.warn(e)
uuid = Honeybadger.notify(e)
redirect_to warehouse_batch_path(@batch), flash: { alert: "Error mapping fields! #{e.message} (please report EID: #{uuid})" }
return
end
redirect_to process_confirm_warehouse_batch_path(@batch), notice: "Field mapping saved. Please review and process your batch."
else
flash.now[:error] = "Failed to save field mapping. #{@batch.errors.full_messages.join(", ")}"
render :map_fields, status: :unprocessable_entity
end
end
private
def batch_params
params.require(:batch).permit(:warehouse_template_id, :warehouse_user_facing_title, :csv, tags: [])
end
def set_allowed_templates
@allowed_templates = Warehouse::Template.where(public: true).or(Warehouse::Template.where(user: current_user))
end
end

View file

@ -0,0 +1,137 @@
class Warehouse::OrdersController < ApplicationController
before_action :set_warehouse_order, except: [:new, :create, :index]
# GET /warehouse/orders or /warehouse/orders.json
def index
authorize Warehouse::Order
# Get all orders with their associations using policy scope
@all_orders = policy_scope(Warehouse::Order).includes(:batch, :address, :source_tag, :user)
# Filter by batched/unbatched based on view parameter
if params[:view] == "batched"
@warehouse_orders = @all_orders.in_batch
else
@warehouse_orders = @all_orders.not_in_batch.page(params[:page]).per(20)
end
end
# GET /warehouse/orders/1 or /warehouse/orders/1.json
def show
authorize @warehouse_order
end
# GET /warehouse/orders/new
def new
authorize Warehouse::Order
@warehouse_order = Warehouse::Order.new
@warehouse_order.build_address
end
# GET /warehouse/orders/1/edit
def edit
authorize @warehouse_order
end
def send_to_warehouse
authorize @warehouse_order
begin
@warehouse_order.dispatch!
rescue Zenventory::ZenventoryError => e
uuid = Honeybadger.notify(e)
redirect_to @warehouse_order, alert: "zenventory said \"#{e.message}\" (please report EID: #{uuid})"
rescue AASM::InvalidTransition => e
uuid = Honeybadger.notify(e)
redirect_to @warehouse_order, alert: "couldn't dispatch order! wrong state? (please report EID: #{uuid})"
end
redirect_to @warehouse_order, flash: { success: "successfully sent to warehouse!" }
end
# POST /warehouse/orders or /warehouse/orders.json
def create
@warehouse_order = Warehouse::Order.new(
warehouse_order_params.merge(
user: current_user,
source_tag: SourceTag.web_tag,
)
)
authorize @warehouse_order
respond_to do |format|
if @warehouse_order.save
format.html { redirect_to @warehouse_order, notice: "Order was successfully created." }
format.json { render :show, status: :created, location: @warehouse_order }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @warehouse_order.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /warehouse/orders/1 or /warehouse/orders/1.json
def update
authorize @warehouse_order
respond_to do |format|
if @warehouse_order.update(warehouse_order_params)
format.html { redirect_to @warehouse_order, notice: "Order was successfully updated." }
format.json { render :show, status: :ok, location: @warehouse_order }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @warehouse_order.errors, status: :unprocessable_entity }
end
end
end
def cancel
authorize @warehouse_order
unless @warehouse_order.may_mark_canceled?
redirect_back_or_to @warehouse_order, alert: "order is not in a cancelable state!"
end
end
def confirm_cancel
authorize @warehouse_order, :cancel?
reason = params.require(:cancellation_reason)
begin
@warehouse_order.cancel!(reason)
rescue Zenventory::ZenventoryError => e
redirect_to @warehouse_order, alert: "couldn't cancel order! zenventory said: #{e.message}"
rescue AASM::InvalidTransition => e
redirect_to @warehouse_order, alert: "couldn't cancel order! wrong state?"
end
end
# # DELETE /warehouse/orders/1 or /warehouse/orders/1.json
def destroy
authorize @warehouse_order
@warehouse_order.destroy!
respond_to do |format|
format.html { redirect_to warehouse_orders_path, status: :see_other, notice: "it's gone." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_warehouse_order
@warehouse_order = Warehouse::Order.find_by!(hc_id: params.expect(:id))
end
# Only allow a list of trusted parameters through.
def warehouse_order_params
params.require(:warehouse_order).permit(
:user_facing_title,
:user_facing_description,
:internal_notes,
:recipient_email,
:notify_on_dispatch,
tags: [],
line_items_attributes: [:id, :sku_id, :quantity, :_destroy],
address_attributes: %i[first_name last_name line_1 line_2 city state postal_code country phone_number email],
).compact_blank
end
end

View file

@ -0,0 +1,68 @@
class Warehouse::SKUsController < ApplicationController
before_action :set_warehouse_sku, only: %i[ show edit update ]
# GET /warehouse/skus or /warehouse/skus.json
def index
authorize Warehouse::SKU
@warehouse_skus = params[:include_non_inventory] ? Warehouse::SKU.all : Warehouse::SKU.in_inventory
end
# GET /warehouse/skus/1 or /warehouse/skus/1.json
def show
authorize @warehouse_sku
end
# GET /warehouse/skus/new
def new
authorize Warehouse::SKU
@warehouse_sku = Warehouse::SKU.new
end
# GET /warehouse/skus/1/edit
def edit
authorize @warehouse_sku
end
# POST /warehouse/skus or /warehouse/skus.json
def create
@warehouse_sku = Warehouse::SKU.new(warehouse_sku_params)
authorize @warehouse_sku
respond_to do |format|
if @warehouse_sku.save
format.html { redirect_to @warehouse_sku, notice: "WarehouseSKU was successfully created." }
format.json { render :show, status: :created, location: @warehouse_sku }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @warehouse_sku.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /warehouse/skus/1 or /warehouse/skus/1.json
def update
authorize @warehouse_sku
respond_to do |format|
if @warehouse_sku.update(warehouse_sku_params)
format.html { redirect_to @warehouse_sku, notice: "WarehouseSKU was successfully updated." }
format.json { render :show, status: :ok, location: @warehouse_sku }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @warehouse_sku.errors, status: :unprocessable_entity }
end
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_warehouse_sku
@warehouse_sku = Warehouse::SKU.find(params.expect(:id))
end
# Only allow a list of trusted parameters through.
def warehouse_sku_params
params.expect(warehouse_sku: [ :sku, :description, :unit_cost, :customs_description, :in_stock, :ai_enabled, :enabled ])
end
end

View file

@ -0,0 +1,80 @@
class Warehouse::TemplatesController < ApplicationController
before_action :set_warehouse_template, only: %i[ show edit update destroy ]
# GET /warehouse/templates or /warehouse/templates.json
def index
authorize Warehouse::Template
@warehouse_templates = Warehouse::Template.all
end
# GET /warehouse/templates/1 or /warehouse/templates/1.json
def show
authorize @warehouse_template
end
# GET /warehouse/templates/new
def new
authorize Warehouse::Template
@warehouse_template = Warehouse::Template.new
end
# GET /warehouse/templates/1/edit
def edit
authorize @warehouse_template
end
# POST /warehouse/templates or /warehouse/templates.json
def create
@warehouse_template = Warehouse::Template.new(warehouse_template_params.merge(user: current_user, source_tag: SourceTag.web_tag))
authorize @warehouse_template
respond_to do |format|
if @warehouse_template.save
format.html { redirect_to @warehouse_template, notice: "Template was successfully created." }
format.json { render :show, status: :created, location: @warehouse_template }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @warehouse_template.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /warehouse/templates/1 or /warehouse/templates/1.json
def update
authorize @warehouse_template
respond_to do |format|
if @warehouse_template.update(warehouse_template_params)
format.html { redirect_to @warehouse_template, notice: "Template was successfully updated." }
format.json { render :show, status: :ok, location: @warehouse_template }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @warehouse_template.errors, status: :unprocessable_entity }
end
end
end
# DELETE /warehouse/templates/1 or /warehouse/templates/1.json
def destroy
authorize @warehouse_template
@warehouse_template.destroy!
respond_to do |format|
format.html { redirect_to warehouse_templates_path, status: :see_other, notice: "Template was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_warehouse_template
@warehouse_template = Warehouse::Template.find(params.require(:id))
end
# Only allow a list of trusted parameters through.
def warehouse_template_params
params.require(:warehouse_template).permit(
:name, :public,
line_items_attributes: [ :id, :sku_id, :quantity, :_destroy ]
)
end
end

View file

@ -0,0 +1,87 @@
require "administrate/base_dashboard"
class AddressDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
city: Field::String,
country: Field::Select.with_options(searchable: false, collection: ->(field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }),
first_name: Field::String,
last_name: Field::String,
line_1: Field::String,
line_2: Field::String,
phone_number: Field::String,
postal_code: Field::String,
state: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
id
city
country
first_name
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
city
country
first_name
last_name
line_1
line_2
phone_number
postal_code
state
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
city
country
first_name
last_name
line_1
line_2
phone_number
postal_code
state
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how addresses are displayed
# across all pages of the admin dashboard.
#
# def display_resource(address)
# "Address ##{address.id}"
# end
end

View file

@ -0,0 +1,66 @@
require "administrate/base_dashboard"
class CommonTagDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
implies_ysws: Field::Boolean,
tag: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
id
implies_ysws
tag
created_at
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
implies_ysws
tag
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
implies_ysws
tag
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how common tags are displayed
# across all pages of the admin dashboard.
#
# def display_resource(common_tag)
# "CommonTag ##{common_tag.id}"
# end
end

View file

@ -0,0 +1,90 @@
require "administrate/base_dashboard"
class ReturnAddressDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
name: Field::String,
line_1: Field::String,
line_2: Field::String,
city: Field::String,
country: Field::Select.with_options(searchable: false, collection: ->(field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }),
postal_code: Field::String,
shared: Field::Boolean,
state: Field::String,
user: Field::BelongsTo.with_options(required: false),
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
name
user
shared
city
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
name
line_1
line_2
city
state
postal_code
country
shared
user
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
line_1
line_2
city
state
postal_code
country
shared
user
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {
shared: ->(resources) { resources.where(shared: true) },
unshared: ->(resources) { resources.where(shared: false) }
}.freeze
# Overwrite this method to customize how return addresses are displayed
# across all pages of the admin dashboard.
#
def display_resource(return_address)
return_address.name
end
end

View file

@ -0,0 +1,72 @@
require "administrate/base_dashboard"
class SourceTagDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
name: Field::String,
owner: Field::String,
slug: Field::String,
warehouse_orders: Field::HasMany,
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
id
name
owner
slug
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
name
owner
slug
warehouse_orders
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
owner
slug
warehouse_orders
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how source tags are displayed
# across all pages of the admin dashboard.
#
def display_resource(source_tag)
source_tag.slug
end
end

View file

@ -0,0 +1,87 @@
require "administrate/base_dashboard"
class UserDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
can_warehouse: Field::Boolean,
email: Field::String,
icon_url: Field::String,
is_admin: Field::Boolean,
slack_id: Field::String,
username: Field::String,
warehouse_templates: Field::HasMany,
home_mid: Field::BelongsTo,
home_return_address: Field::BelongsTo,
created_at: Field::DateTime,
updated_at: Field::DateTime,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
username
is_admin
can_warehouse
email
slack_id
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
can_warehouse
email
icon_url
is_admin
slack_id
username
warehouse_templates
home_mid
home_return_address
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
can_warehouse
email
icon_url
is_admin
slack_id
username
home_mid
home_return_address
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how users are displayed
# across all pages of the admin dashboard.
#
def display_resource(user)
user.username
end
end

View file

@ -0,0 +1,71 @@
require "administrate/base_dashboard"
module USPS
class MailerIdDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
crid: Field::String,
mid: Field::String,
name: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
id
crid
mid
name
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
crid
mid
name
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
crid
mid
name
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how mailer ids are displayed
# across all pages of the admin dashboard.
#
# def display_resource(mailer_id)
# "USPS::MailerId ##{mailer_id.id}"
# end
end
end

View file

@ -0,0 +1,86 @@
require "administrate/base_dashboard"
module USPS
class PaymentAccountDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
account_number: Field::String,
account_type: Field::Select.with_options(searchable: false, collection: ->(field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }),
manifest_mid: Field::String,
name: Field::String,
permit_number: Field::String,
permit_zip: Field::String,
usps_mailer_id: Field::BelongsTo,
created_at: Field::DateTime,
updated_at: Field::DateTime,
ach: Field::Boolean,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
id
account_number
account_type
manifest_mid
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
account_number
account_type
manifest_mid
name
permit_number
permit_zip
usps_mailer_id
created_at
updated_at
ach
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
account_number
account_type
manifest_mid
name
permit_number
permit_zip
usps_mailer_id
ach
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how payment accounts are displayed
# across all pages of the admin dashboard.
#
# def display_resource(payment_account)
# "USPS::PaymentAccount ##{payment_account.id}"
# end
end
end

View file

@ -0,0 +1,74 @@
require "administrate/base_dashboard"
module Warehouse
class LineItemDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
order: Field::BelongsTo,
quantity: Field::Number,
sku: Field::BelongsTo,
template: Field::BelongsTo,
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
id
order
quantity
sku
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
order
quantity
sku
template
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
order
quantity
sku
template
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how line items are displayed
# across all pages of the admin dashboard.
#
# def display_resource(line_item)
# "Warehouse::LineItem ##{line_item.id}"
# end
end
end

Some files were not shown because too many files have changed in this diff Show more