Compare commits

...

26 Commits

Author SHA1 Message Date
d5a396f53a Fixed the tests and apart from one, they all work 2026-05-02 11:10:25 +00:00
5a38d132b8 Fixed some model tests 2026-05-02 10:51:55 +00:00
6ebb7390d5 Just checking in 2026-05-02 10:46:08 +00:00
92ceddc32e database updated to create domain crendtial references 2026-04-30 15:18:51 +00:00
791034f7a4 readme updated 2026-04-27 12:24:18 +00:00
9267adbecb Latest checkin 2026-04-27 12:09:40 +00:00
b6e558e6dd Lets get the tests working 2026-04-23 12:09:35 +00:00
d1222a6baa Added database creation to the readme 2026-04-23 11:41:36 +00:00
02185cac54 Database installed but not working 2026-04-23 11:26:27 +00:00
03b5ab680b Did Bundle update in the wrong dir 2026-04-23 11:19:11 +00:00
bfd075048d Latest bin files 2026-04-23 11:08:50 +00:00
7d2be7f137 Still not working 2026-04-23 11:06:09 +00:00
d963716372 First bit of config done 2026-04-23 10:59:24 +00:00
5991fd1296 Gemspec updated 2026-04-23 10:54:13 +00:00
5d472a6271 Changed Gemfile 2026-04-23 10:41:10 +00:00
e1bd95987b Updated the README.md 2025-06-19 08:31:24 +00:00
f6baae259e Remove the webauthn-ruby stuff. 2024-05-24 08:45:33 +01:00
ba3392fb47 Looking good. Webauthn integration time!! 2024-05-22 09:54:22 +01:00
0edc7d0563 Only create the opt_secret once. If created leave alone. 2024-05-22 06:30:10 +01:00
b72f88f60f I do believe that OTP/MFA is now working. 2024-05-21 12:44:08 +01:00
95675aa5ef Getting there with Devise MFA 2024-05-21 10:13:05 +01:00
32a06d8c61 Webauthn-ruby - very complicated. Park it here! 2024-05-21 09:59:11 +01:00
dcd4efdd4e More CSS simplification
Edit domain names, updates credentials and virtuals
Model domain tests for length and valid characters
2024-05-20 12:41:45 +01:00
781b2bb9f0 Actually fixed the confirm: issue. They are link_to now
Trimmed the CSS.
2024-05-20 11:19:02 +01:00
d3fb547fb1 Before deleting, you'll be asked. 2024-05-20 10:36:41 +01:00
61019d0331 CSS is better
Upgraded to Rails 7.1.3.3
2024-05-20 10:07:57 +01:00
58 changed files with 860 additions and 442 deletions

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

65
Gemfile
View File

@@ -1,12 +1,20 @@
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.3.1"
ruby "4.0.2"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "7.1.3.2"
gem "rails", "8.1.3"
gem "propshaft"
gem "puma", "~> 6.0"
gem "importmap-rails"
gem "turbo-rails"
gem "stimulus-rails"
gem "jbuilder"
gem 'devise'
gem 'devise-two-factor'
gem 'rqrcode'
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails"
@@ -14,35 +22,18 @@ gem "sprockets-rails"
# Use mysql as the database for Active Record
gem "mysql2", "~> 0.5"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", "~> 6.0"
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
gem "tzinfo-data", platforms: %i[ windows jruby ]
# 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"
# Use Redis adapter to run Action Cable in production
# gem "redis", "~> 4.0"
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"
# 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[ mingw mswin x64_mingw 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
gem "thruster", require: false
gem "image_processing", "~> 1.2"
# Use Sass to process CSS
# gem "sassc-rails"
@@ -52,23 +43,25 @@ gem "bootsnap", require: false
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri mingw x64_mingw ]
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
# Audits gems for known security defects (use config/bundler-audit.yml to ignore issues)
gem "bundler-audit", require: false
# 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"
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
# gem "spring"
end
group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara"
gem "selenium-webdriver"
gem "webdrivers"
gem "capybara", "~> 3.40"
gem "selenium-webdriver", "~> 4.43"
end

View File

@@ -1,89 +1,95 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.1.3.2)
actionpack (= 7.1.3.2)
activesupport (= 7.1.3.2)
action_text-trix (2.1.18)
railties
actioncable (8.1.3)
actionpack (= 8.1.3)
activesupport (= 8.1.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.1.3.2)
actionpack (= 7.1.3.2)
activejob (= 7.1.3.2)
activerecord (= 7.1.3.2)
activestorage (= 7.1.3.2)
activesupport (= 7.1.3.2)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.1.3.2)
actionpack (= 7.1.3.2)
actionview (= 7.1.3.2)
activejob (= 7.1.3.2)
activesupport (= 7.1.3.2)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
actionmailbox (8.1.3)
actionpack (= 8.1.3)
activejob (= 8.1.3)
activerecord (= 8.1.3)
activestorage (= 8.1.3)
activesupport (= 8.1.3)
mail (>= 2.8.0)
actionmailer (8.1.3)
actionpack (= 8.1.3)
actionview (= 8.1.3)
activejob (= 8.1.3)
activesupport (= 8.1.3)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.1.3.2)
actionview (= 7.1.3.2)
activesupport (= 7.1.3.2)
actionpack (8.1.3)
actionview (= 8.1.3)
activesupport (= 8.1.3)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actiontext (7.1.3.2)
actionpack (= 7.1.3.2)
activerecord (= 7.1.3.2)
activestorage (= 7.1.3.2)
activesupport (= 7.1.3.2)
useragent (~> 0.16)
actiontext (8.1.3)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.3)
activerecord (= 8.1.3)
activestorage (= 8.1.3)
activesupport (= 8.1.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.1.3.2)
activesupport (= 7.1.3.2)
actionview (8.1.3)
activesupport (= 8.1.3)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.1.3.2)
activesupport (= 7.1.3.2)
activejob (8.1.3)
activesupport (= 8.1.3)
globalid (>= 0.3.6)
activemodel (7.1.3.2)
activesupport (= 7.1.3.2)
activerecord (7.1.3.2)
activemodel (= 7.1.3.2)
activesupport (= 7.1.3.2)
activemodel (8.1.3)
activesupport (= 8.1.3)
activerecord (8.1.3)
activemodel (= 8.1.3)
activesupport (= 8.1.3)
timeout (>= 0.4.0)
activestorage (7.1.3.2)
actionpack (= 7.1.3.2)
activejob (= 7.1.3.2)
activerecord (= 7.1.3.2)
activesupport (= 7.1.3.2)
activestorage (8.1.3)
actionpack (= 8.1.3)
activejob (= 8.1.3)
activerecord (= 8.1.3)
activesupport (= 8.1.3)
marcel (~> 1.0)
activesupport (7.1.3.2)
activesupport (8.1.3)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
json
logger (>= 1.4.2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
base64 (0.2.0)
bcrypt (3.1.20)
bigdecimal (3.1.8)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
base64 (0.3.0)
bcrypt (3.1.22)
bigdecimal (4.1.2)
bindex (0.8.1)
bootsnap (1.18.3)
bootsnap (1.23.0)
msgpack (~> 1.2)
builder (3.2.4)
brakeman (8.0.4)
racc
builder (3.3.0)
bundler-audit (0.9.3)
bundler (>= 1.2.0)
thor (~> 1.0)
capybara (3.40.0)
addressable
matrix
@@ -93,186 +99,298 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
chunky_png (1.4.0)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
crass (1.0.6)
date (3.3.4)
debug (1.9.2)
date (3.5.1)
debug (1.11.1)
irb (~> 1.10)
reline (>= 0.3.8)
devise (4.9.4)
devise (5.0.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
railties (>= 7.0)
responders
warden (~> 1.2.3)
drb (2.2.1)
erubi (1.12.0)
globalid (1.2.1)
devise-two-factor (6.4.0)
activesupport (>= 7.2, < 8.2)
devise (>= 4.0, < 6.0)
railties (>= 7.2, < 8.2)
rotp (~> 6.0)
drb (2.2.3)
erb (6.0.4)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
ffi (1.17.4)
fugit (1.12.1)
et-orbi (~> 1.4)
raabro (~> 1.4)
globalid (1.3.0)
activesupport (>= 6.1)
i18n (1.14.5)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
importmap-rails (2.0.1)
image_processing (1.14.0)
mini_magick (>= 4.9.5, < 6)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (2.2.3)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.7.2)
irb (1.13.1)
io-console (0.8.2)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.12.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
loofah (2.22.0)
jbuilder (2.14.1)
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
json (2.19.4)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.25.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mail (2.9.0)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.4)
matrix (0.4.2)
marcel (1.1.0)
matrix (0.4.3)
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
mini_portile2 (2.8.6)
minitest (5.22.3)
msgpack (1.7.2)
mutex_m (0.2.0)
mysql2 (0.5.6)
net-imap (0.4.11)
mini_portile2 (2.8.9)
minitest (6.0.5)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.0)
mysql2 (0.5.7)
bigdecimal
net-imap (0.6.3)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.0)
net-smtp (0.5.1)
net-protocol
nio4r (2.7.3)
nokogiri (1.16.5)
nio4r (2.7.5)
nokogiri (1.19.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
orm_adapter (0.5.0)
psych (5.1.2)
parallel (2.0.1)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
propshaft (1.3.2)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
psych (5.3.1)
date
stringio
public_suffix (5.0.5)
puma (6.4.2)
public_suffix (7.0.5)
puma (6.6.1)
nio4r (~> 2.0)
racc (1.7.3)
rack (3.0.11)
rack-session (2.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.2.6)
rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.1.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.1.0)
rackup (2.3.1)
rack (>= 3)
webrick (~> 1.8)
rails (7.1.3.2)
actioncable (= 7.1.3.2)
actionmailbox (= 7.1.3.2)
actionmailer (= 7.1.3.2)
actionpack (= 7.1.3.2)
actiontext (= 7.1.3.2)
actionview (= 7.1.3.2)
activejob (= 7.1.3.2)
activemodel (= 7.1.3.2)
activerecord (= 7.1.3.2)
activestorage (= 7.1.3.2)
activesupport (= 7.1.3.2)
rails (8.1.3)
actioncable (= 8.1.3)
actionmailbox (= 8.1.3)
actionmailer (= 8.1.3)
actionpack (= 8.1.3)
actiontext (= 8.1.3)
actionview (= 8.1.3)
activejob (= 8.1.3)
activemodel (= 8.1.3)
activerecord (= 8.1.3)
activestorage (= 8.1.3)
activesupport (= 8.1.3)
bundler (>= 1.15.0)
railties (= 7.1.3.2)
rails-dom-testing (2.2.0)
railties (= 8.1.3)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.1.3.2)
actionpack (= 7.1.3.2)
activesupport (= 7.1.3.2)
irb
rails-html-sanitizer (1.7.0)
loofah (~> 2.25)
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.1.3)
actionpack (= 8.1.3)
activesupport (= 8.1.3)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6)
rake (13.2.1)
rdoc (6.6.3.1)
rainbow (3.1.1)
rake (13.4.2)
rdoc (7.2.0)
erb
psych (>= 4.0.0)
regexp_parser (2.9.1)
reline (0.5.7)
tsort
regexp_parser (2.12.0)
reline (0.6.3)
io-console (~> 0.5)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.2.6)
rubyzip (2.3.2)
selenium-webdriver (4.20.1)
responders (3.2.0)
actionpack (>= 7.0)
railties (>= 7.0)
rexml (3.4.4)
rotp (6.3.0)
rqrcode (3.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.1.0)
rubocop (1.86.1)
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.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-rails (2.34.3)
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-progressbar (1.13.0)
ruby-vips (2.3.0)
ffi (~> 1.12)
logger
rubyzip (3.2.2)
securerandom (0.4.1)
selenium-webdriver (4.43.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
sprockets (4.2.1)
solid_cable (3.0.12)
actioncable (>= 7.2)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_cache (1.0.10)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_queue (1.4.0)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
fugit (~> 1.11)
railties (>= 7.1)
thor (>= 1.3.1)
sprockets (4.2.2)
concurrent-ruby (~> 1.0)
logger
rack (>= 2.2.4, < 4)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets-rails (3.5.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
stimulus-rails (1.3.3)
railties (>= 6.0.0)
stringio (3.1.0)
thor (1.3.1)
timeout (0.4.1)
turbo-rails (2.0.5)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.2.0)
thor (1.5.0)
thruster (0.1.20)
timeout (0.6.1)
tsort (0.2.0)
turbo-rails (2.0.23)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
uri (1.1.1)
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
web-console (4.3.0)
actionview (>= 8.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webdrivers (5.2.0)
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (~> 4.0)
webrick (1.8.1)
websocket (1.2.10)
websocket-driver (0.7.6)
railties (>= 8.0.0)
websocket (1.2.11)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.14)
zeitwerk (2.7.5)
PLATFORMS
x86_64-openbsd
DEPENDENCIES
bootsnap
capybara
brakeman
bundler-audit
capybara (~> 3.40)
debug
devise
devise-two-factor
image_processing (~> 1.2)
importmap-rails
jbuilder
mysql2 (~> 0.5)
propshaft
puma (~> 6.0)
rails (= 7.1.3.2)
selenium-webdriver
rails (= 8.1.3)
rqrcode
rubocop-rails-omakase
selenium-webdriver (~> 4.43)
solid_cable
solid_cache
solid_queue
sprockets-rails
stimulus-rails
thruster
turbo-rails
tzinfo-data
web-console
webdrivers
RUBY VERSION
ruby 3.3.1p55
ruby 4.0.2p0
BUNDLED WITH
2.3.26

View File

@@ -1,10 +1,32 @@
# DO NOT USE - still working on it.
# Ruby on Rails front end for OpenSMTPD.
An ISC Licensed RoR app to manage your OpenSMTPD users.
## OpenBSD Instructions.
doas pkg_add
```
doas pkg_add ruby
ln -sf /usr/local/bin/ruby40 /usr/local/bin/ruby
ln -sf /usr/local/bin/bundle40 /usr/local/bin/bundle
ln -sf /usr/local/bin/bundler40 /usr/local/bin/bundler
ln -sf /usr/local/bin/erb40 /usr/local/bin/erb
ln -sf /usr/local/bin/gem40 /usr/local/bin/gem
ln -sf /usr/local/bin/irb40 /usr/local/bin/irb
ln -sf /usr/local/bin/minitest40 /usr/local/bin/minitest
ln -sf /usr/local/bin/racc40 /usr/local/bin/racc
ln -sf /usr/local/bin/rake40 /usr/local/bin/rake
ln -sf /usr/local/bin/rbs40 /usr/local/bin/rbs
ln -sf /usr/local/bin/rdbg40 /usr/local/bin/rdbg
ln -sf /usr/local/bin/rdoc40 /usr/local/bin/rdoc
ln -sf /usr/local/bin/ri40 /usr/local/bin/ri
ln -sf /usr/local/bin/syntax_suggest40 /usr/local/bin/syntax_suggest
ln -sf /usr/local/bin/test-unit40 /usr/local/bin/test-unit
ln -sf /usr/local/bin/typeprof40 /usr/local/bin/typeprof
```
## Database schema
Is derived from https://github.com/OpenSMTPD/OpenSMTPD-extras/blob/master/extras/tables/table-mysql/table-mysql.5
@@ -12,9 +34,9 @@ Is derived from https://github.com/OpenSMTPD/OpenSMTPD-extras/blob/master/extras
## Prerequisites
* Ruby version - 3.2.3
* Ruby version - 4.0.2
* Rails 7.1.3
* Rails 8.1.3
* System dependencies
@@ -22,7 +44,19 @@ Is derived from https://github.com/OpenSMTPD/OpenSMTPD-extras/blob/master/extras
* Database creation
```
CREATE USER 'OpenSMTPD'@'localhost' IDENTIFIED BY 'password';
CREATE DATABASE OpenSMTPD;
GRANT ALL PRIVILEGES ON OpenSMTPD.* TO 'OpenSMTPD'@'localhost';
FLUSH PRIVILEGES;
```
* Database initialization
```
RAILS_ENV=production rails db:migrate
RAILS_ENV=production rails console
```
* How to run the test suite
@@ -30,12 +64,13 @@ Is derived from https://github.com/OpenSMTPD/OpenSMTPD-extras/blob/master/extras
* Deployment instructions
You will need to create an admin user using the Rails Console:
## Licensed
Copyright 2022 - 2023 Jez Caudle
Copyright 2022 - 2026 Jez Caudle
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

View File

@@ -14,15 +14,73 @@
*= require_self
*/
body, nav, main, footer {
padding: 1rem;
margin: 1rem;
/*
* https://github.com/Lazzzer00/Best-CSS-Reset-2024
*/
*, *::before, *::after{
box-sizing: border-box;
}
*{
margin: 0;
padding: 0;
}
ul[role='list'], ol[role='list']{
list-style: none;
}
html:focus-within{
scroll-behavior: smooth;
}
a:not([class]){
text-decoration-skip-ink: auto;
}
img, picture, svg, video, canvas{
max-width: 100%;
height: auto;
vertical-align: middle;
font-style: italic;
background-repeat: no-repeat;
background-size: cover;
}
input, button, textarea, select{
font: inherit;
}
@media (prefers-reduced-motion: reduce){
html:focus-within {
scroll-behavior: auto;
}
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
transition: none;
}
}
body, html{
height: 100%;
scroll-behavior: smooth;
padding:1rem;
font-family:sans-serif;
}
/*
* Other CSS
*/
menu {
display: flex;
list-style: none;
padding: 1rem;
padding:1rem;
background-color: #daeafa;
border-radius:1rem;
}
@@ -32,36 +90,81 @@ menu > li {
flex-grow: 1;
}
main, .flash {
background-color: #efefef;
border-radius:1rem;
padding:1rem;
}
.flash {
background-color: #dfcedf;
margin-bottom:1rem;
text-align: center;
}
.domain-header, .mfa-header, h2 {
background-color: #fefefe;
border-radius:1rem;
padding:1rem 1rem;
}
.domain-header h2 {
display: flex;
}
.domain-header a {
font-size: 9pt;
scale: 75%;
}
.domain-header button {
margin:0 0 0 10px;
.email-list, .domain-list, .mfa-list, .work-area {
border-radius:1rem;
border:1rem black;
background-color:#e7eae7;
padding:2rem;
margin:1rem 0;
}
.work-area input {
padding:0.5rem;
margin:0.5rem;
}
.work-area label {
padding:0.5rem;
margin:0.5rem;
}
.email-list li {
display: flex;
margin:0.3rem 1rem;
}
.email-list li a {
padding:0 10px;
}
main, body {
padding:1rem;
margin:1rem;
padding:0 0.3rem;
}
footer {
margin-top:1rem;
background-color:#efefef;
margin:0 1rem;
padding:1rem;
border-radius:1rem;
text-align:center;
}
footer ul, footer h3 , footer li {
margin:10px;
}
a, a:visited {
color:blue;
text-decoration:none;
}
a:hover {
color:red;
background-color:yellow;
text-decoration:underline;
}

View File

@@ -1,3 +1,10 @@
class ApplicationController < ActionController::Base
before_action :authenticate_user!
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
end
end

View File

@@ -36,8 +36,21 @@ class DomainsController < ApplicationController
# PATCH/PUT /domains/1 or /domains/1.json
def update
pre_domain_name = @domain.domain
respond_to do |format|
if @domain.update(domain_params)
# now we need to update the credentials and virtuals
@domain.credentials.each do |credential|
credential.email.sub! pre_domain_name, @domain.domain
credential.save
end
@domain.virtuals.each do |virtual|
virtual.email.sub! pre_domain_name, @domain.domain
virtual.save
end
format.html { redirect_to domain_url(@domain), notice: "Domain was successfully updated." }
format.json { render :show, status: :ok, location: @domain }
else

View File

@@ -0,0 +1,22 @@
class MfasController < ApplicationController
def new
issuer = "Hidden Agenda Email"
label = "#{issuer}:#{current_user.email}"
if current_user.otp_secret.to_s.length == 0
current_user.otp_secret = User.generate_otp_secret
current_user.save!
end
qrcode = RQRCode::QRCode.new([{ data: current_user.otp_provisioning_uri(label, issuer: issuer), mode: :byte_8bit }])
@svg = qrcode.as_svg(color: "000", shape_rendering: "crispEdges", module_size: 5, standalone: true,
use_path: true
)
end
def create
current_user.otp_required_for_login = true
current_user.save!
redirect_to root_url
end
end

View File

@@ -5,6 +5,7 @@ class Domain < ApplicationRecord
has_many :virtuals, dependent: :destroy
validates :domain, presence: true
validates :domain, uniqueness: true
validates :domain, format: { with: /\A(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}|[a-z0-9-]{1,30}\.[a-z]{2,})\z/, message: "domain not valid" }
private
def create_abuse_postmaster

View File

@@ -1,5 +1,10 @@
class User < ApplicationRecord
devise :two_factor_authenticatable
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :lockable, :timeoutable, :trackable, :registerable
devise :recoverable, :rememberable, :validatable, :lockable, :timeoutable, :trackable, :registerable
def role
end
end

View File

@@ -1,4 +1,4 @@
<%= form_with(model: [@domain, credential]) do |form| %>
<%= form_with(model: [@domain, credential], html: { class: "work-area"}) do |form| %>
<% if credential.errors.any? %>
<div style="color: red">
<h2><%= pluralize(credential.errors.count, "error") %> prohibited this credential from being saved:</h2>
@@ -11,17 +11,12 @@
</div>
<% end %>
<div>
<%= form.label :email, style: "display: inline" %> @<%=@domain.domain%>
<%= form.text_field :email %>
</div>
<%= form.label :email %>:<%= form.text_field :email %>@<%=@domain.domain%>
<br/>
<%= form.label :password %>:<%= form.text_field :password %>
<br/>
<div>
<%= form.label :password, style: "display: block" %>
<%= form.text_field :password %>
</div>
<div>
<%= form.submit %>
</div>
<%= form.submit "Create email" %>
<% end %>

View File

@@ -1,9 +1,7 @@
<h1>New credential</h1>
<h2>New email for <%= @domain.domain %></h2>
<%= render "form", credential: @credential %>
<br>
<div>
<%= link_to "Back to the domain", domain_path(@domain) %>
</div>
<%= link_to "Back to the domain", domain_path(@domain) %>

View File

@@ -21,5 +21,5 @@
<%= f.submit "Change my password" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
<hr/>
<%#= render "devise/shared/links" %>

View File

@@ -1,39 +1,34 @@
<h2>Edit <%= resource_name.to_s.humanize %></h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'work-area' }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>
<%= f.label :email %>:<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
<div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
<% end %>
<div class="field">
<%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
<%= f.password_field :password, autocomplete: "new-password" %>
<% if @minimum_password_length %>
<br />
<em><%= @minimum_password_length %> characters minimum</em>
<% end %>
</div>
<br/>
<%= f.label :password %>:<%= f.password_field :password, autocomplete: "new-password" %>
<% if @minimum_password_length %><em><%= @minimum_password_length %> characters minimum</em><% end %> <i>(leave blank if you don't want to change it)</i>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
</div>
<br/>
<%= f.label :password_confirmation %>:<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
<div class="field">
<%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
<%= f.password_field :current_password, autocomplete: "current-password" %>
</div>
<br/>
<%= f.label :current_password %>:<%= f.password_field :current_password, autocomplete: "current-password" %>
<br/><i>(we need your current password to confirm your changes)</i>
<div class="actions">
<%= f.submit "Update" %>
</div>
<br/>
<%= f.submit "Update" %>
<% end %>
<% if current_user.otp_secret.to_s.size == 0 %>
<%= link_to "Enable MFA", new_mfa_path %>
<% else %>
<%= link_to "Edit MFA", new_mfa_path %>
<% end %>
|
<%= link_to "Back", :back %>

View File

@@ -1,27 +1,22 @@
<h2>Log in</h2>
<div class="black asap m-l3">
<%= form_for(resource, as: resource_name, url: session_path(resource_name), class: "p-10 border shadow br-b-8 md-w-50pc mx-auto") do |f| %>
<div class="flex flex-column">
<%= f.label :email, class: "my-2" %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class:"input" %>
</div>
<div class="flex flex-column mt-3">
<%= f.label :password, class: "my-2" %>
<%= f.password_field :password, autocomplete: "current-password", class: "input" %>
</div>
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: "work-area" } ) do |f| %>
<%= f.label :email %>:<%= f.email_field :email, autofocus: true, autocomplete: "email", class:"input" %>
<br/>
<%= f.label :password %>:<%= f.password_field :password, autocomplete: "current-password", class: "input" %>
<br/>
<%= f.label :otp_attempt %>:<%= f.password_field :otp_attempt, autocomplete: "OTP Code", class: "input" %>
<br/>
<% if devise_mapping.rememberable? %>
<div class="flex flex-items-center mt-4">
<%= f.check_box :remember_me, class: "checkbox mr-3" %>
<%= f.label :remember_me, class: "form-check-label" %>
</div>
<%= f.label :remember_me %><%= f.check_box :remember_me %>
<% end %>
<div class="mt-4">
<%= f.submit "Log in", class: "button" %>
</div>
<% end %>
</div>
<br/>
<%= f.submit "Log in", class: "button" %>
<% end %>
<%= render "devise/shared/links" %>

View File

@@ -1,4 +1,4 @@
<%= form_with(model: domain) do |form| %>
<%= form_with(model: domain, html: { class: "work-area" }) do |form| %>
<% if domain.errors.any? %>
<div style="color: red">
<h2><%= pluralize(domain.errors.count, "error") %> prohibited this domain from being saved:</h2>
@@ -12,11 +12,13 @@
<% end %>
<div>
<%= form.label :domain, style: "display: block" %>
<%= form.text_field :domain %>
<%= form.label :domain %>:<%= form.text_field :domain %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>

View File

@@ -1,3 +1,5 @@
<div class="domain">
<h1>Editing domain</h1>
<%= render "form", domain: @domain %>
@@ -5,6 +7,8 @@
<br>
<div>
<%= link_to "Show this domain", @domain %> |
<%= link_to "Back to domains", domains_path %>
<%= link_to "Back", @domain %> |
<%= link_to "Home", domains_path %>
</div>
</div>

View File

@@ -1,11 +1,14 @@
<h1>Domains <%= link_to "Add new domain",new_domain_path %> </h1>
<div class="domain">
<div class="domain-header">
<h2>Domains <%= link_to "Add new domain",new_domain_path %></h2>
</div>
<div id="domains">
<ul>
<% @domains.each do |domain| %>
<li><%= link_to domain.domain, domain %></li>
<div class="domain-list">
<ul><% @domains.each do |domain| %>
<li><%= link_to domain.domain, domain %></li>
<% end %>
</ul>
</ul>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<h1>New domain</h1>
<h2>New domain</h2>
<%= render "form", domain: @domain %>
<br>
<div>
<%= link_to "Back to domains", domains_path %>
</div>
<%= link_to "Back to domains", domains_path %>

View File

@@ -1,21 +1,21 @@
<div>
<div class="domain">
<div class="domain-header">
<h2><%= @domain.domain %> <%#= link_to "Edit", edit_domain_path(@domain) %> <%= button_to "Delete", @domain, method: :delete %></h2>
<h2><%= @domain.domain %> <%= link_to "Edit", edit_domain_path(@domain) %> <%= link_to "Delete", @domain, data: { turbo_method: :delete, turbo_confirm: "Are you sure?"} %></h2>
</div>
<div class="email-list">
<h3>Credentials (<%= @domain.credentials.count %>) <%= link_to "Add #{@domain.domain} email address", new_domain_credential_path(@domain) %></h2>
<h3>Credentials (<%= @domain.credentials.count %>) <%= link_to "Add #{@domain.domain} email address", new_domain_credential_path(@domain) %></h3>
<ul>
<% @domain.credentials.each do |credential| %>
<li><%= credential.email %> <%= link_to "Edit", edit_domain_credential_path(@domain,credential) %> <%= button_to "Delete this email address", [@domain, credential], method: :delete %></li>
<li><%= credential.email %> <%= link_to "Edit", edit_domain_credential_path(@domain,credential) %> <%= link_to "Delete this email address", [@domain, credential], data: { turbo_method: :delete, turbo_confirm: "Are you sure?"} %></li>
<% end %>
</ul>
<hr/>
<h3>Virtuals (<%= @domain.virtuals.count %>) <%= link_to "Add #{@domain.domain} virtual email", new_domain_virtual_path(@domain) %></h2>
</div>
<hr/>
<div class="email-list">
<h3>Virtuals (<%= @domain.virtuals.count %>) <%= link_to "Add #{@domain.domain} virtual email", new_domain_virtual_path(@domain) %></h3>
<ul>
<% @domain.virtuals.each do |virtual| %>
<li><%= virtual.email%> >> <%= virtual.destination%> <%= link_to "Edit", edit_domain_virtual_path(@domain,virtual)%> <%= button_to "Delete", [@domain, virtual], method: :delete, style: "display:inline" %></li>
<li><%= virtual.email%> >> <%= virtual.destination%> <%= link_to "Edit", edit_domain_virtual_path(@domain,virtual)%> <%= link_to "Delete", [@domain, virtual], data: { turbo_method: :delete, turbo_confirm: "Are you sure?"} %></li>
<% end %>
</ul>

View File

@@ -19,22 +19,26 @@
<br/>
</div>
<% if notice %><div class="flash"><%= notice %></div><% end %><% if alert %><div class="flash"><%= alert %></div><% end %>
<main>
<% if notice %><div class="bg-green-lightest green-darkest px-5 py-3 br-3 border-l bw-6 bc-green"><%= notice %></div><% end %>
<% if alert %><div class="bg-red-lightest red-darkest px-5 py-3 br-3 border-l bw-6 bc-red"><%= alert %></div><% end %>
<%= yield %>
</main>
<% if Rails.env == "development" %>
<footer>
<hr/>
RoR Version <%= Rails.version %> (<%=Rails.env%>) | Ruby <%= "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}" %> | OS <%= RUBY_PLATFORM %> | App Version <%= `git describe --always` %>
<% if user_signed_in? %>
<hr/>
<%= "User:#{current_user.email} | OTP for login:#{current_user.otp_required_for_login} | " %>
<% end %>
<!--
<h3>To-Do (In order of importance):</h3>
<ul>
<li>Tests for controllers and integration</li>
<li>2FA</li>
</ul>
-->
<% end %>
</footer>
</body>
</html>

View File

@@ -0,0 +1,9 @@
<div class="mfa">
<h1 class="mfa-header">New MFA</h1>
<div class="mfa-list">
<p>Scan the code below and then click "Done".</p>
<p>You will only be able to login with your authenticator app once you have clicked "Done"</p>
<%= @svg.html_safe%>
<p><%= link_to "Done", mfas_path, data: { turbo_method: :post} %></p>
</div>
</div>

View File

@@ -1,4 +1,4 @@
<%= form_with(model: [@domain,virtual]) do |form| %>
<%= form_with(model: [@domain,virtual], html: { class: "work-area"}) do |form| %>
<% if virtual.errors.any? %>
<div style="color: red">
<h2><%= pluralize(virtual.errors.count, "error") %> prohibited this virtual from being saved:</h2>
@@ -11,17 +11,12 @@
</div>
<% end %>
<div>
<%= form.label :email, style: "display: block" %>
<%= form.text_field :email %>
</div>
<%= form.label :email%>:<%= form.text_field :email %>@<%=@domain.domain%>
<br/>
<div>
<%= form.label :destination, style: "display: block" %>
<%= form.text_field :destination %>
</div>
<%= form.label :destination %>:<%= form.text_field :destination %>
<br/>
<%= form.submit %>
<div>
<%= form.submit %>
</div>
<% end %>

View File

@@ -1,9 +1,8 @@
<h1>New virtual for <%= @domain.domain %></h1>
<h2>New virtual for <%= @domain.domain %></h2>
<%= render "form", virtual: @virtual %>
<br>
<div>
<%= link_to "Back to #{@domain.domain}", domain_path(@domain) %>
</div>
<%= link_to "Back to #{@domain.domain}", domain_path(@domain) %>

7
bin/brakeman Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env ruby
require "rubygems"
require "bundler/setup"
ARGV.unshift("--ensure-latest")
load Gem.bin_path("brakeman", "brakeman")

6
bin/bundler-audit Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "bundler/audit/cli"
ARGV.concat %w[ --config config/bundler-audit.yml ] if ARGV.empty? || ARGV.include?("check")
Bundler::Audit::CLI.start

6
bin/ci Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "active_support/continuous_integration"
CI = ActiveSupport::ContinuousIntegration
require_relative "../config/ci.rb"

2
bin/dev Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env ruby
exec "./bin/rails", "server", *ARGV

8
bin/docker-entrypoint Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash -e
# If running the rails server then create or migrate existing database
if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
./bin/rails db:prepare
fi
exec "${@}"

8
bin/rubocop Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env ruby
require "rubygems"
require "bundler/setup"
# Explicit RuboCop config increases performance slightly while avoiding config confusion.
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
load Gem.bin_path("rubocop", "rubocop")

View File

@@ -1,11 +1,10 @@
#!/usr/bin/env ruby
require "fileutils"
# path to your application root.
APP_ROOT = File.expand_path("..", __dir__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
system(*args, exception: true)
end
FileUtils.chdir APP_ROOT do
@@ -14,7 +13,6 @@ FileUtils.chdir APP_ROOT do
# Add necessary setup steps to this file.
puts "== Installing dependencies =="
system! "gem install bundler --conservative"
system("bundle check") || system!("bundle install")
# puts "\n== Copying sample files =="
@@ -24,10 +22,14 @@ FileUtils.chdir APP_ROOT do
puts "\n== Preparing database =="
system! "bin/rails db:prepare"
system! "bin/rails db:reset" if ARGV.include?("--reset")
puts "\n== Removing old logs and tempfiles =="
system! "bin/rails log:clear tmp:clear"
puts "\n== Restarting application server =="
system! "bin/rails restart"
unless ARGV.include?("--skip-server")
puts "\n== Starting development server =="
STDOUT.flush # flush the output before exec(2) so that it displays
exec "bin/dev"
end
end

5
bin/thrust Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env ruby
require "rubygems"
require "bundler/setup"
load Gem.bin_path("thruster", "thrust")

View File

@@ -9,7 +9,8 @@ Bundler.require(*Rails.groups)
module OpensmtpdRailsFrontend
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
config.load_defaults 8.1
config.autoload_lib(ignore: %w[assets tasks])
# Configuration for the application, engines, and railties goes here.
#

View File

@@ -1 +1 @@
RYdwaEGe4bkkqfzKaJgRzzktgKS227tgbhMhGS1aSa69LRZauHIiIBnqQSOSNrRftPa65+QkuSTToq7VvQklLwUPDluIXOudx/IxCjvnmjoVdkBDPFzCIJhM01eD4ZKje6cqTIMQqVCAlZ0dCHuOrdLaU4TiH9qqKN9KqoKfrV9O8IXVV0x7xt8um1KDyT7XQ/JQS6ggmSbiqvkNhwuflgrYJFzMfX4/glcCTSviFzhW0ri4XOA6LYnQhYF6ed8z/mNaiVM1Q2yBspVNIxCFwQEs8kF/VWmVjSUdPFExR3Tt0j/w7zc4e9LlxbHvPcJg43f6de3Wn2pJ8DQbc3HB8Lg29ENKqwSSoMwzQUfL6LP96pClZ4vz7XsjApMtYHINH/fxeplNAyfS9bniTmWw3z78mXI2OBK1cxH5--Hxh1AHoj2eUcIPCJ--MDVZpDlIQA0rY04Lx86jsg==
16XC4dR/e6ZfMDLdM5A1IWRXPbWK5XG6e3/4HXV5cKxtkL/tYSfw+JduP8XpJtH0+paiRcQ+KZvLNndy+U+K/u9GkimSKGC1u/3iIreWGH44NW5G6DpG02kqa+7F27Vt0vyK0LKG+eipwuusUi5lxlVYWV1NoxCyEgDhh0oylMBZTs5eiD82CUpXcOOi/Y6WII3XfmCa0JTn1FvMKtX8HKWUMzCJXowBhZ9MkPpjEENzBPVn5N0MbWsuwTygcABE0KyuCISpwCUrIuNFnQLhNoPAApo2MD1sAiPjDb/xVV9rwSiSqTzaovjkXadGnhDa+7gh5LRTqgpn5yuBm19ud9JVTflVomXQooHrByJEMRSWJ1BsLro7R+3eEqCsHtbgLjQ0Vpa+moO4XTvzdMOtpZvpMjY3zlHatWCEsjBXVZaTL2ptmfXjVjXscZOge6Jomw8+VjAJaAPSnR8l+O+pTu3n4oCQ270YIZhGDk+3m2zUkqb8uHDy/0pulMUSFUSI6UV7A46V061H17bnAERzHIQiFQkBLbmFWEvsPG6mMi6J7J7ynRc5/6avrj+9mKr6/REWwQijJwQTRMsSlzTaGQY6ecUX4lDxS3kzVxccqYophPR9tAwOwxjfTWyrLtQPdTPaGkh6rUcQO36BF5A=--vGTJqJldqOOCgx5I--NEnAbiMfebeJNQd9LIBAOg==

View File

@@ -13,20 +13,20 @@ default: &default
adapter: mysql2
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
username: OpenSMTPD
password: password
host: localhost
development:
<<: *default
database: opensmtpd_dev
database: OpenSMTPD
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: opensmtpd_test
database: OpenSMTPD_test
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
@@ -50,6 +50,6 @@ test:
#
production:
<<: *default
database: opensmtpd
username: opensmtpd
database: OpenSMTPD
username: OpenSMTPD
password: <%= ENV["OPENSMTPD_RAILS_FRONTEND_DATABASE_PASSWORD"] %>

View File

@@ -3,10 +3,8 @@ require "active_support/core_ext/integer/time"
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded any time
# it changes. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
# Make code changes take effect immediately without server restart.
config.enable_reloading = true
# Do not eager load code on boot.
config.eager_load = false
@@ -14,48 +12,52 @@ Rails.application.configure do
# Show full error reports.
config.consider_all_requests_local = true
# Enable server timing
# Enable server timing.
config.server_timing = true
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
# Enable/disable Action Controller caching. By default Action Controller caching is disabled.
# Run rails dev:cache to toggle Action Controller caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{2.days.to_i}"
}
config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
else
config.action_controller.perform_caching = false
config.cache_store = :null_store
end
# Change to :null_store to avoid any caching.
config.cache_store = :memory_store
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
# Make template changes take effect immediately.
config.action_mailer.perform_caching = false
# Set localhost to be used by links generated in mailer templates.
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
# Append comments with runtime information tags to SQL queries in logs.
config.active_record.query_log_tags_enabled = true
# Highlight code that enqueued background job in logs.
config.active_job.verbose_enqueue_logs = true
# Highlight code that triggered redirect in logs.
config.action_dispatch.verbose_redirect_logs = true
# Suppress logger output for asset requests.
config.assets.quiet = true
@@ -63,10 +65,19 @@ Rails.application.configure do
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
config.action_view.annotate_rendered_view_with_filenames = true
# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
# Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
# config.generators.apply_rubocop_autocorrect_after_generate!
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
end
#

View File

@@ -5,3 +5,4 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin "@github/webauthn-json", to: "@github--webauthn-json.js" # @2.1.1

View File

@@ -6,7 +6,3 @@ Rails.application.config.assets.version = "1.0"
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )

View File

@@ -1,8 +1,8 @@
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header
# Rails.application.configure do
# config.content_security_policy do |policy|
@@ -16,11 +16,14 @@
# # policy.report_uri "/csp-violation-report-endpoint"
# end
#
# # Generate session nonces for permitted importmap and inline scripts
# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
# config.content_security_policy_nonce_directives = %w(script-src)
# config.content_security_policy_nonce_directives = %w(script-src style-src)
#
# # Report CSP violations to a specified URI. See:
# # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
# # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
# # config.content_security_policy_nonce_auto = true
#
# # Report violations without enforcing the policy.
# # config.content_security_policy_report_only = true
# end

View File

@@ -9,6 +9,10 @@
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :two_factor_authenticatable
end
# The secret key used by Devise. Devise uses this key to generate
# random tokens. Changing this key will render invalid all existing
# confirmation, reset password and unlock tokens in the database.
@@ -228,7 +232,7 @@ Devise.setup do |config|
# When set to false, does not sign a user in automatically after their password is
# reset. Defaults to true, so a user is signed in automatically after a reset.
# config.sign_in_after_reset_password = true
config.sign_in_after_reset_password = false
# ==> Configuration for :encryptable
# Allow you to use another hashing or encryption algorithm besides bcrypt (default).

View File

@@ -1,8 +1,8 @@
# Be sure to restart your server when you modify this file.
# Configure parameters to be filtered from the log file. Use this to limit dissemination of
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
# notations and behaviors.
# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
:passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
]

View File

@@ -1,44 +1,47 @@
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
# Specifies the `worker_timeout` threshold that Puma will use to wait before
# terminating a worker in development environments.
#bind 'tcp://0.0.0.0:3002'
# This configuration file will be evaluated by Puma. The top-level methods that
# are invoked here are part of Puma's configuration DSL. For more information
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
#
worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
# Puma starts a configurable number of processes (workers) and each process
# serves each request in a thread from an internal thread pool.
#
# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You
# should only set this value when you want to run 2 or more workers. The
# default is already 1. You can set it to `auto` to automatically start a worker
# for each available processor.
#
# The ideal number of threads per worker depends both on how much time the
# application spends waiting for IO operations and on how much you wish to
# prioritize throughput over latency.
#
# As a rule of thumb, increasing the number of threads will increase how much
# traffic a given process can handle (throughput), but due to CRuby's
# Global VM Lock (GVL) it has diminishing returns and will degrade the
# response time (latency) of the application.
#
# The default is set to 3 threads as it's deemed a decent compromise between
# throughput and latency for the average Rails application.
#
# Any libraries that use a connection pool or another resource pool should
# be configured to provide at least as many connections as the number of
# threads. This includes Active Record's `pool` parameter in `database.yml`.
threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch("PORT") { 3001 }
# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }
# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
# preload_app!
port ENV.fetch("PORT", 3000)
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
bind 'tcp://0.0.0.0:3002'
# Run the Solid Queue supervisor inside of Puma for single-server deployments.
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]
# Specify the PID file. Defaults to tmp/pids/server.pid in development.
# In other environments, only set the PID file if requested.
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]

View File

@@ -11,6 +11,8 @@ Rails.application.routes.draw do
put 'users' => 'devise/registrations#update', :as => 'user_registration'
end
resources :mfas, only: [:new, :create]
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")

View File

@@ -1,8 +0,0 @@
class AddDomainIdToVirtuals < ActiveRecord::Migration[7.0]
def change
add_column :virtuals, :domain_id, :integer
add_column :credentials, :domain_id, :integer
add_index :virtuals, :domain_id
add_index :credentials, :domain_id
end
end

View File

@@ -0,0 +1,7 @@
class AddDeviseTwoFactorToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :otp_secret, :string
add_column :users, :consumed_timestep, :integer
add_column :users, :otp_required_for_login, :boolean
end
end

View File

@@ -0,0 +1,8 @@
class CreateDomainsAdmins < ActiveRecord::Migration[8.1]
def change
create_table :domains_admins, id: false do |t|
t.belongs_to :users
t.belongs_to :domains
end
end
end

View File

@@ -0,0 +1,5 @@
class AddDomainRedToCredentials < ActiveRecord::Migration[8.1]
def change
add_reference :credentials, :domain, null: false, foreign_key: true
end
end

View File

@@ -0,0 +1,5 @@
class AddDomainRedToVirtuals < ActiveRecord::Migration[8.1]
def change
add_reference :virtuals, :domain, null: false, foreign_key: true
end
end

55
db/schema.rb generated
View File

@@ -10,50 +10,63 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_06_21_060115) do
create_table "credentials", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
ActiveRecord::Schema[8.1].define(version: 2026_04_30_152237) do
create_table "credentials", charset: "latin1", collation: "latin1_swedish_ci", force: :cascade do |t|
t.datetime "created_at", null: false
t.bigint "domain_id", null: false
t.string "email"
t.string "password"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "domain_id"
t.index ["domain_id"], name: "index_credentials_on_domain_id"
end
create_table "domains", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.string "domain"
create_table "domains", charset: "latin1", collation: "latin1_swedish_ci", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "domain"
t.datetime "updated_at", null: false
end
create_table "users", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
create_table "domains_admins", id: false, charset: "latin1", collation: "latin1_swedish_ci", force: :cascade do |t|
t.bigint "domains_id"
t.bigint "users_id"
t.index ["domains_id"], name: "index_domains_admins_on_domains_id"
t.index ["users_id"], name: "index_domains_admins_on_users_id"
end
create_table "users", charset: "latin1", collation: "latin1_swedish_ci", force: :cascade do |t|
t.integer "consumed_timestep"
t.datetime "created_at", null: false
t.datetime "current_sign_in_at"
t.string "current_sign_in_ip"
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.integer "failed_attempts", default: 0, null: false
t.string "unlock_token"
t.datetime "last_sign_in_at"
t.string "last_sign_in_ip"
t.datetime "locked_at"
t.datetime "created_at", null: false
t.boolean "otp_required_for_login"
t.string "otp_secret"
t.datetime "remember_created_at"
t.datetime "reset_password_sent_at"
t.string "reset_password_token"
t.integer "role", default: 1000, null: false
t.integer "sign_in_count", default: 0, null: false
t.string "unlock_token"
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
end
create_table "virtuals", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.string "email"
t.string "destination"
create_table "virtuals", charset: "latin1", collation: "latin1_swedish_ci", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "destination"
t.bigint "domain_id", null: false
t.string "email"
t.datetime "updated_at", null: false
t.integer "domain_id"
t.index ["domain_id"], name: "index_virtuals_on_domain_id"
end
add_foreign_key "credentials", "domains"
add_foreign_key "virtuals", "domains"
end

View File

@@ -5,7 +5,7 @@ class CredentialsControllerTest < ActionDispatch::IntegrationTest
setup do
@credential = credentials(:one)
@domain = domains(:one)
@domain = domains(:domainone)
end
test "should not get new" do

View File

@@ -3,7 +3,7 @@ require "test_helper"
class DomainsControllerTest < ActionDispatch::IntegrationTest
include Devise::Test::IntegrationHelpers
setup do
@domain = domains(:one)
@domain = domains(:domainone)
end
test "not should get index because user not logged in" do

View File

@@ -5,7 +5,7 @@ class VirtualsControllerTest < ActionDispatch::IntegrationTest
setup do
@virtual = virtuals(:one)
@domain = domains(:one)
@domain = domains(:domainone)
end
test "should get not get new because we are not logged in" do

View File

@@ -3,9 +3,9 @@
one:
email: bob@example.net
password: MyString
domain: one
domain: domainone
two:
email: alice@example.org
password: MyString
domain: two
domain: domaintwo

View File

@@ -1,7 +1,7 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
domainone:
domain: example.net
two:
domaintwo:
domain: example.org

View File

@@ -19,7 +19,12 @@ class CredentialTest < ActiveSupport::TestCase
end
test "domain added to name" do
@c = Credential.new(email: "jez@#{domains(:one).domain}", password: "1234567&ab", domain_id: domains(:one).id)
@c = Credential.new(email: "jez@#{domains(:domainone).domain}", password: "1234567&ab", domain_id: domains(:domainone).id)
assert @c.save
end
test "domain_id not the same as domain" do
@c = Credential.new(email: "steve@#{domains(:domainone).domain}", password: "1234567&ab", domain_id: domains(:domainone).id)
assert_not @c.save
end
end

View File

@@ -22,4 +22,16 @@ class DomainTest < ActiveSupport::TestCase
assert_equal(@d.virtuals.count,2)
end
test "valid domain names only" do
@d = Domain.new(domain: "Spaces Spaces.co.uk")
assert_not @d.save
@d.domain = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789.com"
assert_not @d.save
end
end

View File

@@ -43,7 +43,7 @@ class VirtualTest < ActiveSupport::TestCase
@v = Virtual.new
@v.email = "abuse@example.net"
@v.destination = "valid@email.com"
@v.domain_id = domains(:one).id + 1234
@v.domain_id = domains(:domainone).id + 1234
assert !@v.save
end

View File

@@ -0,0 +1,2 @@
function base64urlToBuffer(e){const r="==".slice(0,(4-e.length%4)%4);const t=e.replace(/-/g,"+").replace(/_/g,"/")+r;const n=atob(t);const i=new ArrayBuffer(n.length);const o=new Uint8Array(i);for(let e=0;e<n.length;e++)o[e]=n.charCodeAt(e);return i}function bufferToBase64url(e){const r=new Uint8Array(e);let t="";for(const e of r)t+=String.fromCharCode(e);const n=btoa(t);const i=n.replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"");return i}var e="copy";var r="convert";function convert(t,n,i){if(n===e)return i;if(n===r)return t(i);if(n instanceof Array)return i.map((e=>convert(t,n[0],e)));if(n instanceof Object){const e={};for(const[r,o]of Object.entries(n)){if(o.derive){const e=o.derive(i);void 0!==e&&(i[r]=e)}if(r in i)null!=i[r]?e[r]=convert(t,o.schema,i[r]):e[r]=null;else if(o.required)throw new Error(`Missing key: ${r}`)}return e}}function derived(e,r){return{required:true,schema:e,derive:r}}function required(e){return{required:true,schema:e}}function optional(e){return{required:false,schema:e}}var t={type:required(e),id:required(r),transports:optional(e)};var n={appid:optional(e),appidExclude:optional(e),credProps:optional(e)};var i={appid:optional(e),appidExclude:optional(e),credProps:optional(e)};var o={publicKey:required({rp:required(e),user:required({id:required(r),name:required(e),displayName:required(e)}),challenge:required(r),pubKeyCredParams:required(e),timeout:optional(e),excludeCredentials:optional([t]),authenticatorSelection:optional(e),attestation:optional(e),extensions:optional(n)}),signal:optional(e)};var a={type:required(e),id:required(e),rawId:required(r),authenticatorAttachment:optional(e),response:required({clientDataJSON:required(r),attestationObject:required(r),transports:derived(e,(e=>{var r;return(null==(r=e.getTransports)?void 0:r.call(e))||[]}))}),clientExtensionResults:derived(i,(e=>e.getClientExtensionResults()))};var u={mediation:optional(e),publicKey:required({challenge:required(r),timeout:optional(e),rpId:optional(e),allowCredentials:optional([t]),userVerification:optional(e),extensions:optional(n)}),signal:optional(e)};var s={type:required(e),id:required(e),rawId:required(r),authenticatorAttachment:optional(e),response:required({clientDataJSON:required(r),authenticatorData:required(r),signature:required(r),userHandle:required(r)}),clientExtensionResults:derived(i,(e=>e.getClientExtensionResults()))};var c={credentialCreationOptions:o,publicKeyCredentialWithAttestation:a,credentialRequestOptions:u,publicKeyCredentialWithAssertion:s};function createRequestFromJSON(e){return convert(base64urlToBuffer,o,e)}function createResponseToJSON(e){return convert(bufferToBase64url,a,e)}async function create(e){const r=await navigator.credentials.create(createRequestFromJSON(e));return createResponseToJSON(r)}function getRequestFromJSON(e){return convert(base64urlToBuffer,u,e)}function getResponseToJSON(e){return convert(bufferToBase64url,s,e)}async function get(e){const r=await navigator.credentials.get(getRequestFromJSON(e));return getResponseToJSON(r)}function supported(){return!!(navigator.credentials&&navigator.credentials.create&&navigator.credentials.get&&window.PublicKeyCredential)}export{create,get,c as schema,supported};