Merge pull request #476 from OWASP/rails-8-upgrade

Upgrade to Ruby 3.3.6 and Rails 8.0.4
This commit is contained in:
Ken Johnson
2025-12-07 22:38:22 +00:00
committed by GitHub
37 changed files with 4065 additions and 1957 deletions
+1 -1
View File
@@ -1 +1 @@
2.6.5 3.3.6
+14 -17
View File
@@ -1,33 +1,31 @@
# frozen_string_literal: true # frozen_string_literal: true
source "https://rubygems.org" source "https://rubygems.org"
#don't upgrade gem "rails", "~> 8.0.0"
gem "rails", "6.0.0"
ruby "2.6.5" ruby "3.3.6"
gem "aruba" gem "aruba"
gem "bcrypt" gem "bcrypt"
gem "coffee-rails"
gem "execjs"
gem "foreman" gem "foreman"
gem "jquery-fileupload-rails" gem "jquery-fileupload-rails"
gem "jquery-rails" gem "jquery-rails"
gem "minitest" gem "minitest"
gem "powder" # Pow related gem
gem "pry-rails" # not in dev group in case running via prod/staging @ a training gem "pry-rails" # not in dev group in case running via prod/staging @ a training
gem "puma" gem "puma", "~> 6.0"
gem "rails-perftest"
gem "rake" gem "rake"
gem "responders" #For Rails 4.2 # LOCKED DOWN gem "responders"
gem "ruby-prof" gem "ruby-prof"
gem "sassc-rails" gem "sassc-rails"
gem "simplecov", require: false, group: :test gem "simplecov", require: false, group: :test
gem "sqlite3" gem "sqlite3", "~> 2.0"
gem "therubyracer"
gem "turbolinks" gem "turbolinks"
gem "uglifier"
gem "unicorn" # Asset pipeline
gem "sprockets-rails"
gem "importmap-rails"
gem "stimulus-rails"
gem "turbo-rails"
# Add SMTP server support using MailCatcher # Add SMTP server support using MailCatcher
# NOTE: https://github.com/sj26/mailcatcher#bundler # NOTE: https://github.com/sj26/mailcatcher#bundler
@@ -43,16 +41,15 @@ group :development, :mysql do
gem "pry" gem "pry"
gem "rack-livereload" gem "rack-livereload"
gem "rb-fsevent" gem "rb-fsevent"
gem "rubocop-github" gem "rubocop"
gem "travis-lint"
end end
group :development, :test, :mysql do group :development, :test, :mysql do
gem "capybara" gem "capybara"
gem "database_cleaner" gem "database_cleaner"
gem "launchy" gem "launchy"
gem "poltergeist" gem "selenium-webdriver"
gem "rspec-rails", '4.0.0.beta3' # 4/26/2019: LOCKED DOWN gem "rspec-rails"
gem "test-unit" gem "test-unit"
end end
+413 -284
View File
@@ -1,143 +1,181 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.0.0) actioncable (8.0.4)
actionpack (= 6.0.0) actionpack (= 8.0.4)
activesupport (= 8.0.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.0.0) zeitwerk (~> 2.6)
actionpack (= 6.0.0) actionmailbox (8.0.4)
activejob (= 6.0.0) actionpack (= 8.0.4)
activerecord (= 6.0.0) activejob (= 8.0.4)
activestorage (= 6.0.0) activerecord (= 8.0.4)
activesupport (= 6.0.0) activestorage (= 8.0.4)
mail (>= 2.7.1) activesupport (= 8.0.4)
actionmailer (6.0.0) mail (>= 2.8.0)
actionpack (= 6.0.0) actionmailer (8.0.4)
actionview (= 6.0.0) actionpack (= 8.0.4)
activejob (= 6.0.0) actionview (= 8.0.4)
mail (~> 2.5, >= 2.5.4) activejob (= 8.0.4)
rails-dom-testing (~> 2.0) activesupport (= 8.0.4)
actionpack (6.0.0) mail (>= 2.8.0)
actionview (= 6.0.0) rails-dom-testing (~> 2.2)
activesupport (= 6.0.0) actionpack (8.0.4)
rack (~> 2.0) actionview (= 8.0.4)
rack-test (>= 0.6.3) activesupport (= 8.0.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.0)
actionpack (= 6.0.0)
activerecord (= 6.0.0)
activestorage (= 6.0.0)
activesupport (= 6.0.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.0.0) rack (>= 2.2.4)
activesupport (= 6.0.0) 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.4)
actionpack (= 8.0.4)
activerecord (= 8.0.4)
activestorage (= 8.0.4)
activesupport (= 8.0.4)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.4)
activesupport (= 8.0.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.11)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.6)
activejob (6.0.0) activejob (8.0.4)
activesupport (= 6.0.0) activesupport (= 8.0.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.0.0) activemodel (8.0.4)
activesupport (= 6.0.0) activesupport (= 8.0.4)
activerecord (6.0.0) activerecord (8.0.4)
activemodel (= 6.0.0) activemodel (= 8.0.4)
activesupport (= 6.0.0) activesupport (= 8.0.4)
activestorage (6.0.0) timeout (>= 0.4.0)
actionpack (= 6.0.0) activestorage (8.0.4)
activejob (= 6.0.0) actionpack (= 8.0.4)
activerecord (= 6.0.0) activejob (= 8.0.4)
marcel (~> 0.3.1) activerecord (= 8.0.4)
activesupport (6.0.0) activesupport (= 8.0.4)
concurrent-ruby (~> 1.0, >= 1.0.2) marcel (~> 1.0)
i18n (>= 0.7, < 2) activesupport (8.0.4)
minitest (~> 5.1) base64
tzinfo (~> 1.1) benchmark (>= 0.3)
zeitwerk (~> 2.1, >= 2.1.8) bigdecimal
addressable (2.7.0) concurrent-ruby (~> 1.0, >= 1.3.1)
public_suffix (>= 2.0.2, < 5.0) connection_pool (>= 2.2.5)
aruba (0.14.12) drb
childprocess (>= 0.6.3, < 4.0.0) i18n (>= 1.6, < 2)
contracts (~> 0.9) logger (>= 1.4.2)
cucumber (>= 1.3.19) minitest (>= 5.1)
ffi (~> 1.9) securerandom (>= 0.3)
rspec-expectations (>= 2.99) tzinfo (~> 2.0, >= 2.0.5)
thor (~> 0.19) uri (>= 0.13.1)
ast (2.4.0) addressable (2.8.8)
backports (3.15.0) public_suffix (>= 2.0.2, < 8.0)
bcrypt (3.1.13) aruba (2.3.2)
better_errors (2.5.1) bundler (>= 1.17, < 3.0)
coderay (>= 1.0.0) contracts (>= 0.16.0, < 0.18.0)
cucumber (>= 8.0, < 11.0)
rspec-expectations (>= 3.4, < 5.0)
thor (~> 1.0)
ast (2.4.3)
base64 (0.3.0)
bcrypt (3.1.20)
benchmark (0.5.0)
better_errors (2.10.1)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
binding_of_caller (0.8.0) rouge (>= 1.0.0)
debug_inspector (>= 0.0.1) bigdecimal (3.3.1)
builder (3.2.3) binding_of_caller (1.0.1)
bundler-audit (0.6.1) debug_inspector (>= 1.2.0)
bundler (>= 1.2.0, < 3) builder (3.3.0)
thor (~> 0.18) bundler-audit (0.9.3)
capybara (3.29.0) bundler (>= 1.2.0)
thor (~> 1.0)
capybara (3.40.0)
addressable addressable
matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.11)
rack (>= 1.6.0) rack (>= 1.6.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (~> 1.5) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
childprocess (3.0.0) childprocess (5.1.0)
cliver (0.3.2) logger (~> 1.5)
coderay (1.1.2) coderay (1.1.3)
coffee-rails (5.0.0) concurrent-ruby (1.3.5)
coffee-script (>= 2.2.0) connection_pool (3.0.1)
railties (>= 5.2.0) contracts (0.17.2)
coffee-script (2.4.1) crass (1.0.6)
coffee-script-source cucumber (10.1.1)
execjs base64 (~> 0.2)
coffee-script-source (1.12.2) builder (~> 3.2)
concurrent-ruby (1.1.5) cucumber-ci-environment (> 9, < 11)
contracts (0.16.0) cucumber-core (> 15, < 17)
crass (1.0.5) cucumber-cucumber-expressions (> 17, < 19)
cucumber (3.1.2) cucumber-html-formatter (> 20.3, < 22)
builder (>= 2.1.2) diff-lcs (~> 1.5)
cucumber-core (~> 3.2.0) logger (~> 1.6)
cucumber-expressions (~> 6.0.1) mini_mime (~> 1.1)
cucumber-wire (~> 0.0.1) multi_test (~> 1.1)
diff-lcs (~> 1.3) sys-uname (~> 1.3)
gherkin (~> 5.1.0) cucumber-ci-environment (10.0.1)
multi_json (>= 1.7.5, < 2.0) cucumber-core (15.3.0)
multi_test (>= 0.1.2) cucumber-gherkin (> 27, < 35)
cucumber-core (3.2.1) cucumber-messages (> 26, < 30)
backports (>= 3.8.0) cucumber-tag-expressions (> 5, < 9)
cucumber-tag_expressions (~> 1.1.0) cucumber-cucumber-expressions (18.0.1)
gherkin (~> 5.0) bigdecimal
cucumber-expressions (6.0.1) cucumber-gherkin (34.0.0)
cucumber-tag_expressions (1.1.1) cucumber-messages (> 25, < 29)
cucumber-wire (0.0.1) cucumber-html-formatter (21.15.1)
database_cleaner (1.7.0) cucumber-messages (> 19, < 28)
debug_inspector (0.0.3) cucumber-messages (27.2.0)
diff-lcs (1.3) cucumber-tag-expressions (8.1.0)
docile (1.3.2) database_cleaner (2.1.0)
em-websocket (0.5.1) database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.2.2)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0)
database_cleaner-core (2.0.1)
date (3.5.0)
debug_inspector (1.2.0)
diff-lcs (1.6.2)
docile (1.4.1)
drb (2.2.3)
em-websocket (0.5.3)
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0)
erubi (1.9.0) erb (6.0.0)
erubi (1.13.1)
eventmachine (1.2.7) eventmachine (1.2.7)
execjs (2.7.0) ffi (1.17.2-aarch64-linux-gnu)
ffi (1.11.1) ffi (1.17.2-aarch64-linux-musl)
foreman (0.86.0) ffi (1.17.2-arm-linux-gnu)
formatador (0.2.5) ffi (1.17.2-arm-linux-musl)
gherkin (5.1.0) ffi (1.17.2-arm64-darwin)
globalid (0.4.2) ffi (1.17.2-x86_64-darwin)
activesupport (>= 4.2.0) ffi (1.17.2-x86_64-linux-gnu)
guard (2.16.1) ffi (1.17.2-x86_64-linux-musl)
foreman (0.90.0)
thor (~> 1.4)
formatador (1.2.3)
reline
globalid (1.3.0)
activesupport (>= 6.1)
guard (2.19.1)
formatador (>= 0.2.4) formatador (>= 0.2.4)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
logger (~> 1.6)
lumberjack (>= 1.0.12, < 2.0) lumberjack (>= 1.0.12, < 2.0)
nenv (~> 0.1) nenv (~> 0.1)
notiffany (~> 0.0) notiffany (~> 0.0)
pry (>= 0.9.12) ostruct (~> 0.6)
pry (>= 0.13.0)
shellany (~> 0.0) shellany (~> 0.0)
thor (>= 0.18.1) thor (>= 0.18.1)
guard-compat (1.2.1) guard-compat (1.2.1)
@@ -150,152 +188,216 @@ GEM
guard (~> 2.1) guard (~> 2.1)
guard-compat (~> 1.1) guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0) rspec (>= 2.99.0, < 4.0)
guard-shell (0.7.1) guard-shell (0.7.2)
guard (>= 2.0.0) guard (>= 2.0.0)
guard-compat (~> 1.0) guard-compat (~> 1.0)
http_parser.rb (0.6.0) http_parser.rb (0.8.0)
i18n (1.7.0) i18n (1.14.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
jaro_winkler (1.5.4) importmap-rails (2.2.2)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.8.1)
irb (1.15.3)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jquery-fileupload-rails (1.0.0) jquery-fileupload-rails (1.0.0)
actionpack (>= 3.1) actionpack (>= 3.1)
railties (>= 3.1) railties (>= 3.1)
sassc sassc
jquery-rails (4.3.5) jquery-rails (4.6.1)
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (2.3.1) json (2.17.1)
kgio (2.11.2) language_server-protocol (3.17.0.5)
launchy (2.4.3) launchy (3.1.1)
addressable (~> 2.3) addressable (~> 2.8)
libv8 (3.16.14.19) childprocess (~> 5.0)
listen (3.2.0) logger (~> 1.6)
lint_roller (1.1.0)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.3.1) logger (1.7.0)
loofah (2.24.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.12.0)
lumberjack (1.0.13) lumberjack (1.4.2)
mail (2.7.1) mail (2.9.0)
logger
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
marcel (0.3.3) net-imap
mimemagic (~> 0.3.2) net-pop
method_source (0.9.2) net-smtp
mimemagic (0.3.9) marcel (1.1.0)
nokogiri (~> 1) matrix (0.4.3)
rake memoist3 (1.0.0)
mini_mime (1.0.2) method_source (1.1.0)
mini_portile2 (2.4.0) mini_mime (1.1.5)
minitest (5.13.0) minitest (5.26.2)
multi_json (1.14.1) multi_json (1.18.0)
multi_test (0.1.2) multi_test (1.1.0)
mysql2 (0.5.2) mysql2 (0.5.7)
bigdecimal
nenv (0.3.0) nenv (0.3.0)
nio4r (2.5.2) net-imap (0.5.12)
nokogiri (1.10.10) date
mini_portile2 (~> 2.4.0) net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-musl)
racc (~> 1.4)
notiffany (0.1.3) notiffany (0.1.3)
nenv (~> 0.1) nenv (~> 0.1)
shellany (~> 0.0) shellany (~> 0.0)
parallel (1.18.0) ostruct (0.6.3)
parser (2.6.5.0) parallel (1.27.0)
ast (~> 2.4.0) parser (3.3.10.0)
pg (1.2.3) ast (~> 2.4.1)
poltergeist (1.18.1) racc
capybara (>= 2.1, < 4) pg (1.6.2)
cliver (~> 0.3.1) pg (1.6.2-aarch64-linux)
websocket-driver (>= 0.2.0) pg (1.6.2-aarch64-linux-musl)
powder (0.4.0) pg (1.6.2-arm64-darwin)
thor (>= 0.11.5) pg (1.6.2-x86_64-darwin)
power_assert (1.1.5) pg (1.6.2-x86_64-linux)
pry (0.12.2) pg (1.6.2-x86_64-linux-musl)
coderay (~> 1.1.0) power_assert (3.0.1)
method_source (~> 0.9.0) pp (0.6.3)
pry-rails (0.3.9) prettyprint
pry (>= 0.10.4) prettyprint (0.2.0)
public_suffix (4.0.1) prism (1.6.0)
puma (4.3.5) pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
pry-rails (0.3.11)
pry (>= 0.13.0)
psych (5.2.6)
date
stringio
public_suffix (7.0.0)
puma (6.6.1)
nio4r (~> 2.0) nio4r (~> 2.0)
rack (2.2.3) racc (1.8.1)
rack-livereload (0.3.17) rack (3.1.19)
rack rack-livereload (0.6.1)
rack-test (1.1.0) rack (>= 3.0, < 3.2)
rack (>= 1.0, < 3) rack-session (2.1.1)
rails (6.0.0) base64 (>= 0.1.0)
actioncable (= 6.0.0) rack (>= 3.0.0)
actionmailbox (= 6.0.0) rack-test (2.2.0)
actionmailer (= 6.0.0) rack (>= 1.3)
actionpack (= 6.0.0) rackup (2.2.1)
actiontext (= 6.0.0) rack (>= 3)
actionview (= 6.0.0) rails (8.0.4)
activejob (= 6.0.0) actioncable (= 8.0.4)
activemodel (= 6.0.0) actionmailbox (= 8.0.4)
activerecord (= 6.0.0) actionmailer (= 8.0.4)
activestorage (= 6.0.0) actionpack (= 8.0.4)
activesupport (= 6.0.0) actiontext (= 8.0.4)
bundler (>= 1.3.0) actionview (= 8.0.4)
railties (= 6.0.0) activejob (= 8.0.4)
sprockets-rails (>= 2.0.0) activemodel (= 8.0.4)
rails-dom-testing (2.0.3) activerecord (= 8.0.4)
activesupport (>= 4.2.0) activestorage (= 8.0.4)
activesupport (= 8.0.4)
bundler (>= 1.15.0)
railties (= 8.0.4)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0) rails-html-sanitizer (1.6.2)
loofah (~> 2.3) loofah (~> 2.21)
rails-perftest (0.0.7) 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 (6.0.0) railties (8.0.4)
actionpack (= 6.0.0) actionpack (= 8.0.4)
activesupport (= 6.0.0) activesupport (= 8.0.4)
method_source irb (~> 1.13)
rake (>= 0.8.7) rackup (>= 1.0.0)
thor (>= 0.20.3, < 2.0) rake (>= 12.2)
rainbow (3.0.0) thor (~> 1.0, >= 1.2.2)
raindrops (0.19.0) tsort (>= 0.2)
rake (13.0.0) zeitwerk (~> 2.6)
rb-fsevent (0.10.3) rainbow (3.1.1)
rb-inotify (0.10.0) rake (13.3.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0) ffi (~> 1.0)
ref (2.0.0) rdoc (6.16.1)
regexp_parser (1.6.0) erb
responders (3.0.0) psych (>= 4.0.0)
actionpack (>= 5.0) tsort
railties (>= 5.0) regexp_parser (2.11.3)
rspec (3.9.0) reline (0.6.3)
rspec-core (~> 3.9.0) io-console (~> 0.5)
rspec-expectations (~> 3.9.0) responders (3.2.0)
rspec-mocks (~> 3.9.0) actionpack (>= 7.0)
rspec-core (3.9.0) railties (>= 7.0)
rspec-support (~> 3.9.0) rexml (3.4.4)
rspec-expectations (3.9.0) rouge (4.6.1)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.6)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0) rspec-support (~> 3.13.0)
rspec-mocks (3.9.0) rspec-mocks (3.13.7)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0) rspec-support (~> 3.13.0)
rspec-rails (4.0.0.beta3) rspec-rails (8.0.2)
actionpack (>= 4.2) actionpack (>= 7.2)
activesupport (>= 4.2) activesupport (>= 7.2)
railties (>= 4.2) railties (>= 7.2)
rspec-core (~> 3.8) rspec-core (~> 3.13)
rspec-expectations (~> 3.8) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.8) rspec-mocks (~> 3.13)
rspec-support (~> 3.8) rspec-support (~> 3.13)
rspec-support (3.9.0) rspec-support (3.13.6)
rubocop (0.76.0) rubocop (1.81.7)
jaro_winkler (~> 1.5.1) json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.6) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-github (0.13.0) rubocop-ast (1.48.0)
rubocop (~> 0.70) parser (>= 3.3.7.2)
rubocop-performance (~> 1.3.0) prism (~> 1.4)
rubocop-performance (1.3.0) ruby-prof (1.7.2)
rubocop (>= 0.68.0) base64
ruby-prof (1.0.0) ruby-progressbar (1.13.0)
ruby-progressbar (1.10.1) rubyzip (3.2.2)
sassc (2.2.1) sassc (2.4.0)
ffi (~> 1.9) ffi (~> 1.9)
sassc-rails (2.1.2) sassc-rails (2.1.2)
railties (>= 4.0.0) railties (>= 4.0.0)
@@ -303,50 +405,81 @@ GEM
sprockets (> 3.0) sprockets (> 3.0)
sprockets-rails sprockets-rails
tilt tilt
securerandom (0.4.1)
selenium-webdriver (4.38.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
shellany (0.0.1) shellany (0.0.1)
simplecov (0.17.1) simplecov (0.22.0)
docile (~> 1.1) docile (~> 1.1)
json (>= 1.8, < 3) simplecov-html (~> 0.11)
simplecov-html (~> 0.10.0) simplecov_json_formatter (~> 0.1)
simplecov-html (0.10.2) simplecov-html (0.13.2)
sprockets (4.0.0) simplecov_json_formatter (0.1.4)
sprockets (4.2.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) logger
sprockets-rails (3.2.1) rack (>= 2.2.4, < 4)
actionpack (>= 4.0) sprockets-rails (3.5.2)
activesupport (>= 4.0) actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (1.4.1) sqlite3 (2.8.1-aarch64-linux-gnu)
test-unit (3.3.4) sqlite3 (2.8.1-aarch64-linux-musl)
sqlite3 (2.8.1-arm-linux-gnu)
sqlite3 (2.8.1-arm-linux-musl)
sqlite3 (2.8.1-arm64-darwin)
sqlite3 (2.8.1-x86_64-darwin)
sqlite3 (2.8.1-x86_64-linux-gnu)
sqlite3 (2.8.1-x86_64-linux-musl)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.9)
sys-uname (1.4.1)
ffi (~> 1.1)
memoist3 (~> 1.0.0)
test-unit (3.7.3)
power_assert power_assert
therubyracer (0.12.3) thor (1.4.0)
libv8 (~> 3.16.14.15) tilt (2.6.1)
ref timeout (0.4.4)
thor (0.20.3) tsort (0.2.0)
thread_safe (0.3.6) turbo-rails (2.0.20)
tilt (2.0.10) actionpack (>= 7.1.0)
travis-lint (2.0.0) railties (>= 7.1.0)
json
turbolinks (5.2.1) turbolinks (5.2.1)
turbolinks-source (~> 5.2) turbolinks-source (~> 5.2)
turbolinks-source (5.2.0) turbolinks-source (5.2.0)
tzinfo (1.2.5) tzinfo (2.0.6)
thread_safe (~> 0.1) concurrent-ruby (~> 1.0)
uglifier (4.2.0) unicode-display_width (3.2.0)
execjs (>= 0.3.0, < 3) unicode-emoji (~> 4.1)
unicode-display_width (1.6.0) unicode-emoji (4.1.0)
unicorn (5.5.1) uri (1.1.1)
kgio (~> 2.6) useragent (0.16.11)
raindrops (~> 0.7) websocket (1.2.11)
websocket-driver (0.7.1) websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.2.1) zeitwerk (2.7.3)
PLATFORMS PLATFORMS
ruby 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 DEPENDENCIES
aruba aruba
@@ -355,45 +488,41 @@ DEPENDENCIES
binding_of_caller binding_of_caller
bundler-audit bundler-audit
capybara capybara
coffee-rails
database_cleaner database_cleaner
execjs
foreman foreman
guard-livereload guard-livereload
guard-rspec guard-rspec
guard-shell guard-shell
importmap-rails
jquery-fileupload-rails jquery-fileupload-rails
jquery-rails jquery-rails
launchy launchy
minitest minitest
mysql2 mysql2
pg pg
poltergeist
powder
pry pry
pry-rails pry-rails
puma puma (~> 6.0)
rack-livereload rack-livereload
rails (= 6.0.0) rails (~> 8.0.0)
rails-perftest
rake rake
rb-fsevent rb-fsevent
responders responders
rspec-rails (= 4.0.0.beta3) rspec-rails
rubocop-github rubocop
ruby-prof ruby-prof
sassc-rails sassc-rails
selenium-webdriver
simplecov simplecov
sqlite3 sprockets-rails
sqlite3 (~> 2.0)
stimulus-rails
test-unit test-unit
therubyracer turbo-rails
travis-lint
turbolinks turbolinks
uglifier
unicorn
RUBY VERSION RUBY VERSION
ruby 2.6.5p114 ruby 3.3.6p108
BUNDLED WITH BUNDLED WITH
1.17.3 2.5.22
@@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.
@@ -1,3 +0,0 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
+10 -9
View File
@@ -2,7 +2,7 @@
class AdminController < ApplicationController class AdminController < ApplicationController
before_action :administrative, if: :admin_param, except: [:get_user] before_action :administrative, if: :admin_param, except: [:get_user]
skip_before_action :has_info skip_before_action :has_info
layout false, only: [:get_all_users, :get_user] layout false, only: [:get_all_users]
def dashboard def dashboard
end end
@@ -38,10 +38,11 @@ class AdminController < ApplicationController
pass = params[:user][:password] pass = params[:user][:password]
user.password = pass if !(pass.blank?) user.password = pass if !(pass.blank?)
user.save! user.save!
message = true flash[:success] = "User updated successfully"
end redirect_to admin_get_all_users_path(current_user.id)
respond_to do |format| else
format.json { render json: { msg: message ? "success" : "failure"} } flash[:error] = "User not found"
redirect_to admin_get_all_users_path(current_user.id)
end end
end end
@@ -51,11 +52,11 @@ class AdminController < ApplicationController
# Call destroy here so that all association records w/ id are destroyed as well # Call destroy here so that all association records w/ id are destroyed as well
# Example user.retirement records would be destroyed # Example user.retirement records would be destroyed
user.destroy user.destroy
message = true flash[:success] = "User deleted successfully"
end else
respond_to do |format| flash[:error] = "Cannot delete this user"
format.json { render json: { msg: message ? "success" : "failure"} }
end end
redirect_to admin_get_all_users_path(current_user.id)
end end
private private
+186 -1
View File
@@ -3,5 +3,190 @@ class TutorialsController < ApplicationController
skip_before_action :has_info skip_before_action :has_info
skip_before_action :authenticated skip_before_action :authenticated
layout false, only: [:credentials] def credentials
# Render credentials page with layout
end
# VULNERABILITY: Regular Expression Denial of Service (ReDoS)
# This endpoint demonstrates how malicious input can cause catastrophic backtracking
# in regular expressions, potentially hanging the application.
#
# In Rails 8, Regexp.timeout is set to 1 second by default, which prevents
# infinite hangs but still allows attackers to consume server resources.
#
# Tutorial: See wiki R8-A1-ReDoS for exploitation details
def redos_email
email = params[:email]
# VULNERABLE: Complex email regex with nested quantifiers
# This pattern is susceptible to catastrophic backtracking
email_pattern = /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/
begin
start_time = Time.now
is_valid = email =~ email_pattern
elapsed_time = Time.now - start_time
render json: {
valid: is_valid.present?,
time_elapsed: elapsed_time,
message: "Email validation completed"
}
rescue Regexp::TimeoutError => e
elapsed_time = Time.now - start_time
Rails.logger.warn "[SECURITY] ReDoS attempt detected - pattern: email validation, elapsed: #{elapsed_time}s"
render json: {
error: "Timeout",
message: "Email validation timed out - possible ReDoS attack",
time_elapsed: elapsed_time
}, status: :bad_request
end
end
# VULNERABILITY: ReDoS with nested quantifiers
# Even worse than the email example - this demonstrates pure nested quantifiers
# which cause exponential backtracking.
#
# Tutorial: See wiki R8-A1-ReDoS for exploitation details
def redos_username
username = params[:username]
# EXTREMELY VULNERABLE: Nested quantifiers (a+)+
# This is the canonical ReDoS example
username_pattern = /^(a+)+$/
begin
start_time = Time.now
is_valid = username =~ username_pattern
elapsed_time = Time.now - start_time
render json: {
valid: is_valid.present?,
time_elapsed: elapsed_time,
message: "Username validation completed"
}
rescue Regexp::TimeoutError => e
elapsed_time = Time.now - start_time
Rails.logger.warn "[SECURITY] ReDoS attempt detected - pattern: username validation, elapsed: #{elapsed_time}s"
render json: {
error: "Timeout",
message: "Username validation timed out - possible ReDoS attack",
time_elapsed: elapsed_time
}, status: :bad_request
end
end
# SECURE: Fixed version using simpler regex
# This shows the proper way to validate without ReDoS risk
def redos_email_safe
email = params[:email]
# SAFE: Use Ruby's built-in URI email regex or simple validation
begin
start_time = Time.now
is_valid = email =~ URI::MailTo::EMAIL_REGEXP
elapsed_time = Time.now - start_time
render json: {
valid: is_valid.present?,
time_elapsed: elapsed_time,
message: "Email validation completed (safe method)"
}
rescue Regexp::TimeoutError => e
# This should never happen with the built-in regex, but handle it anyway
elapsed_time = Time.now - start_time
render json: {
error: "Timeout",
message: "Validation timed out",
time_elapsed: elapsed_time
}, status: :bad_request
end
end
# VULNERABILITY A03:2025 - Software Supply Chain Failures
# This endpoint demonstrates various supply chain security issues
#
# Tutorial: See wiki for A03 exploitation details
def supply_chain
render json: {
vulnerabilities: [
{
type: "Missing Subresource Integrity (SRI)",
location: "app/views/layouts/application.html.erb",
description: "CDN assets loaded without integrity checks",
impact: "If CDN is compromised, malicious code can be injected",
cve_example: "Similar to British Airways breach (2018) via Magecart"
},
{
type: "Outdated Dependencies",
location: "Gemfile.lock",
description: "Application may use gems with known vulnerabilities",
impact: "Exploitable CVEs in dependencies",
mitigation: "Run 'bundle audit' to check for known vulnerabilities"
},
{
type: "No Dependency Integrity Validation",
location: "Gemfile / bundler configuration",
description: "Gemfile.lock can be modified without detection",
impact: "Malicious dependencies could be injected",
mitigation: "Use checksums, verify signatures, implement SBOM"
},
{
type: "Insecure Gem Sources",
location: "Gemfile (if misconfigured)",
description: "Using HTTP instead of HTTPS for gem sources",
impact: "Man-in-the-middle attacks during bundle install",
note: "RailsGoat correctly uses HTTPS, but many apps don't"
},
{
type: "No Software Bill of Materials (SBOM)",
location: "Project root",
description: "Missing SBOM documentation",
impact: "Cannot track supply chain components or vulnerabilities",
mitigation: "Generate SBOM using CycloneDX or SPDX formats"
}
],
demo: "Check application.html.erb for CDN assets without SRI",
secure_example: {
vulnerable: '<script src="https://cdn.example.com/lib.js"></script>',
secure: '<script src="https://cdn.example.com/lib.js" integrity="sha384-hash" crossorigin="anonymous"></script>'
}
}
end
# Demonstrate checking for vulnerable dependencies
def check_dependencies
begin
# In a real scenario, this would run bundle-audit or similar
# For demo purposes, we'll return example vulnerability data
render json: {
status: "scan_complete",
message: "This endpoint simulates dependency vulnerability scanning",
note: "Run 'bundle audit' or 'bundle-audit check' in your terminal",
example_vulnerabilities: [
{
gem: "rails",
version: "8.0.4",
advisory: "Check https://rubysec.com for any advisories",
severity: "varies"
},
{
gem: "nokogiri",
note: "Commonly has CVEs, check current version against advisories",
resources: "https://github.com/sparklemotion/nokogiri/security/advisories"
}
],
recommended_tools: [
"bundle-audit - https://github.com/rubysec/bundler-audit",
"Dependabot - https://github.com/dependabot",
"Snyk - https://snyk.io",
"OWASP Dependency-Check"
]
}
rescue => e
render json: { error: e.message }, status: :internal_server_error
end
end
end end
+57 -41
View File
@@ -1,54 +1,70 @@
<div class="dashboard-wrapper"> <div class="container-fluid">
<div class="main-container"> <div class="row">
<div class="row-fluid"> <div class="col-12">
<div class="span12"> <!-- Success Alert -->
<div id="success" style="display: none;" class="alert alert-block alert-success fade in"> <div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<h4 class="alert-heading">Success!</h4> <div class="d-flex align-items-center">
<p>User information successfully updated.</p> <i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
</div> <div>
</div> <h5 class="alert-heading mb-1">Success!</h5>
</div> <p class="mb-0">User information successfully updated.</p>
<div class="row-fluid">
<div class="span12">
<div id="failure" style="display: none;" class="alert alert-block alert-error fade in">
<h4 class="alert-heading">Error!</h4>
<p>Something went wrong.</p>
</div>
</div>
</div>
<div class="row-fluid">
<div class="span12">
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe071;"></span>Manage Users
</div>
</div> </div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="userDataTable" class="widget-body"> <!-- Error Alert -->
</div> <!-- End widget-body--> <div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
</div> <!-- End widget header--> <div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Error!</h5>
<p class="mb-0">Something went wrong. Please try again.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<!-- User Management Card -->
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-people-fill text-primary"></i> Manage Users
</h4>
</div>
<div id="userDataTable" class="card-body p-0">
<!-- Loading state -->
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading users...</span>
</div>
<p class="text-muted mt-3">Loading user data...</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<%= javascript_include_tag "jquery.dataTables.min.js"%> <%= javascript_include_tag "jquery.dataTables.min.js" %>
<script type="text/javascript"> <script type="text/javascript">
function makeActive() {
function makeActive(){
$('li[id="admin"]').addClass('active'); $('li[id="admin"]').addClass('active');
}; }
function loadTable(){ function loadTable() {
$("#userDataTable").load("/admin/"+ <%= params[:admin_id] %> + "/get_all_users") $("#userDataTable").load("/admin/" + <%= params[:admin_id] %> + "/get_all_users");
}; }
$(document).ready( $(document).ready(function() {
makeActive, makeActive();
loadTable() loadTable();
); });
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
</script> </script>
+1 -10
View File
@@ -29,27 +29,18 @@
<%= u.admin ? %{<span class="fs1" aria-label="check" data-icon="&#xe0fe;"}.html_safe : nil %> <%= u.admin ? %{<span class="fs1" aria-label="check" data-icon="&#xe0fe;"}.html_safe : nil %>
</td> </td>
<td> <td>
<%= link_to "Edit", "#", {:onClick => "javascript:openEditModal(#{u.id});", :role => "button", :style => "width:70px", :class => "btn btn-inverse", "data-toggle" => "modal"}%> <%= link_to "Edit", admin_get_user_path(u.id), {:style => "width:70px", :class => "btn btn-inverse"}%>
</td> </td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<div id="editAcct" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel1" aria-hidden="true">
</div>
<div class="clearfix"> <div class="clearfix">
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
function openEditModal(id){
var link = '/admin/'+ id +'/get_user';
$("#editAcct").load(link);
$("#editAcct").modal('show');
};
function dataTablePagination(){ function dataTablePagination(){
$('#data-table').dataTable({ $('#data-table').dataTable({
"sPaginationType": "full_numbers" "sPaginationType": "full_numbers"
+47 -88
View File
@@ -1,96 +1,55 @@
<!-- Begin Modal --> <div class="container-fluid">
<div class="modal-header"> <div class="row">
<button type="button" class="close" data-dismiss="modal" aria-label="dismiss"> <div class="col-lg-8 mx-auto">
× <div class="card shadow-sm mt-4">
</button> <div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
<h4 id="myModalLabel1"> <h4 class="mb-0">
Account Settings <i class="bi bi-person-gear text-primary"></i> Edit User Account
</h4> </h4>
</div> <%= link_to "Back to Users", admin_get_all_users_path(current_user.id), class: "btn btn-outline-secondary btn-sm" %>
<div class="modal-body"> </div>
<div class="row-fluid"> <div class="card-body p-4">
<div class="span8"> <%= form_for @user, url: admin_update_user_path(params[:admin_id]), method: :patch do |f| %>
<%= form_for @user, :html => {:id => "account_edit"} do |f| %> <div class="mb-3">
<div class="control-group"> <%= f.label :email, nil, {:class => "form-label"}%>
<%= f.label :email, nil, {:class => "control-label"}%> <%= f.text_field :email, {:class => "form-control"}%>
<%= f.text_field :email, {:class => "span12"}%> </div>
</div>
<div class="control-group"> <div class="mb-3">
<%= f.label :first_name, nil, {:class => "control-label"}%> <%= f.label :first_name, nil, {:class => "form-label"}%>
<%= f.text_field :first_name, {:class => "span12"} %> <%= f.text_field :first_name, {:class => "form-control"} %>
</div> </div>
<div class="control-group"> <div class="mb-3">
<%= f.label :last_name, nil, {:class => "control-label"}%> <%= f.label :last_name, nil, {:class => "form-label"}%>
<%= f.text_field :last_name, {:class => "span12"} %> <%= f.text_field :last_name, {:class => "form-control"} %>
</div> </div>
<div class="control-group"> <div class="mb-3">
<%= f.label :password, nil, {:class => "control-label"}%> <%= f.label :password, "Password (leave blank to keep current)", {:class => "form-label"}%>
<%= f.password_field :password, {:class => "span12", :placeholder => "Enter Password"}%> <%= f.password_field :password, {:class => "form-control", :placeholder => "Enter new password"}%>
</div> </div>
<div class="control-group"> <div class="mb-3">
<%= f.label :password_confirmation, nil, {:class => "control-label"}%> <%= f.label :password_confirmation, nil, {:class => "form-label"}%>
<%= f.password_field :password_confirmation, {:class => "span12", :placeholder => "Enter Password"} %> <%= f.password_field :password_confirmation, {:class => "form-control", :placeholder => "Confirm new password"} %>
</div> </div>
<%= f.label :admin, nil, {:class => "control-label"}%> <div class="mb-4">
<%= f.select(:admin, @admin_select) %> <%= f.label :admin, "Administrator", {:class => "form-label"}%>
<%= f.select(:admin, @admin_select, {}, {:class => "form-select"}) %>
</div>
<div class="d-flex justify-content-between">
<%= link_to "Delete User", admin_delete_user_path(params[:admin_id]), method: :post, data: { confirm: "Are you sure you want to delete this user?" }, class: "btn btn-danger" %>
<div>
<%= link_to "Cancel", admin_get_all_users_path(current_user.id), class: "btn btn-secondary me-2" %>
<%= f.submit "Save Changes", class: "btn btn-primary" %>
</div>
</div>
<% end %>
</div> </div>
</div> </div>
<div class="row-fluid">
</div>
</div> </div>
<div class="modal-footer"> </div>
<button class="btn" data-dismiss="modal" aria-hidden="true"> </div>
Close
</button>
<%= link_to "Delete", "#", {:id => "delete_button", :class => "btn btn-danger pull-left"} %>
<%= f.submit "Submit", {:id => 'submit_button', :class => "btn btn-primary pull-right"} %>
</div>
<% end %>
<!-- End Modal -->
<%= javascript_include_tag ('validation.js')%>
<script type="text/javascript">
$('#submit_button').click(function() {
var valuesToSubmit = $("#account_edit").serialize();
$("#editAcct").modal('hide');
$.ajax({
url: "/admin/" + <%= @user.id %> + "/update_user.json",
data: valuesToSubmit,
type: "POST",
success: function(response) {
$('#success').show(500).delay(1500).fadeOut();
loadTable();
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
}
});
});
$('#delete_button').click(function() {
$("#editAcct").modal('hide');
$.ajax({
url: "/admin/" + <%= params[:admin_id] %> + "/delete_user.json",
type: "POST",
success: function(response) {
$('#success').show(500).delay(1500).fadeOut();
loadTable();
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
}
});
});
</script>
+214 -110
View File
@@ -1,108 +1,137 @@
<div class="dashboard-wrapper"> <div class="container-fluid">
<div class="main-container"> <div class="row mb-4">
<div class="col-12">
<div class="row-fluid"> <h2 class="mb-3">
<i class="bi bi-file-earmark-medical text-primary"></i> Benefit Forms
<div class="span4"> </h2>
<div class="widget"> <p class="text-muted">Download benefit documents and upload completed forms</p>
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe023;"></span> Health Insurance
</div>
</div>
<!-- Begin Widget Body -->
<div class="widget-body">
Click on PDF to download<br/><br/>
<%= link_to download_path(:type => "File", :name => "public/docs/Health_n_Stuff.pdf") do %>
<div class="doc-icons-container">
<div class="icon light-blue hidden-tablet">
<span class="fs1 doc-icon" aria-hidden="true" data-icon="&#xe1b2;"></span>
<span class="doc-type">
PDF
</span>
</div>
</div>
<% end %>
</div>
<!-- End Widget Body -->
</div>
</div>
<div class="span4">
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe023;"></span> Dental Insurance
</div>
</div>
<!-- Begin Widget Body -->
<div class="widget-body">
Click on PDF to download<br/><br/>
<%= link_to download_path(:type => "File", :name => "public/docs/Dental_n_Stuff.pdf") do %>
<div class="doc-icons-container">
<div class="icon light-blue hidden-tablet">
<span class="fs1 doc-icon" aria-hidden="true" data-icon="&#xe1b2;"></span>
<span class="doc-type">
PDF
</span>
</div>
</div>
<% end %>
</div>
<!-- End Widget Body -->
</div>
</div>
</div> </div>
<div class="row-fluid"> </div>
<div class="span12">
<div class="widget"> <!-- Download Forms Section -->
<div class="widget-header"> <div class="row g-3 mb-4">
<div class="title"> <!-- Health Insurance Card -->
<span class="fs1" aria-hidden="true" data-icon="&#xe023;"></span> Health Insurance <div class="col-lg-6">
</div> <div class="card shadow-sm h-100 hover-card">
</div> <div class="card-body text-center p-4">
<!-- Begin Widget Body --> <div class="mb-3">
<div class="widget-body"> <i class="bi bi-heart-pulse-fill" style="font-size: 3rem; color: var(--rg-primary);"></i>
<div> </div>
<h2>Upload file</h2> <h4 class="card-title mb-3">Health Insurance</h4>
<%= form_for @benefits, :url => upload_path, :html => { :action => "upload", :multipart => true, :id => "fi" } do |f| %> <p class="text-muted mb-4">Download your health insurance benefit forms and information</p>
<!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload --> <%= link_to download_path(type: "File", name: "public/docs/Health_n_Stuff.pdf"), class: "btn btn-primary btn-lg" do %>
<div> <i class="bi bi-file-earmark-pdf"></i> Download PDF
<div> <% end %>
<%= hidden_field "benefits", "backup", :value => false %> </div>
<!-- The fileinput-button span is used to style the file input field as button --> </div>
<span class="btn btn-success fileinput-button"> </div>
<i class="icon-plus icon-white"></i>
<span>Add file</span> <!-- Dental Insurance Card -->
<%= f.file_field :upload %> <div class="col-lg-6">
</span> <div class="card shadow-sm h-100 hover-card">
<button id="start_upload" type="submit" class="btn btn-primary start"> <div class="card-body text-center p-4">
<i class="icon-upload icon-white"></i> <div class="mb-3">
<span><%= t('fileupload.start_upload') %></span> <i class="bi bi-emoji-smile-fill" style="font-size: 3rem; color: var(--rg-success);"></i>
</button> </div>
<br/><br/><span class="filename">Nothing selected</span> <h4 class="card-title mb-3">Dental Insurance</h4>
</div> <p class="text-muted mb-4">Download your dental insurance benefit forms and information</p>
<div class="span5"> <%= link_to download_path(type: "File", name: "public/docs/Dental_n_Stuff.pdf"), class: "btn btn-success btn-lg" do %>
<!-- The global progress bar --> <i class="bi bi-file-earmark-pdf"></i> Download PDF
<div class="progress progress-success progress-striped active fade"> <% end %>
<div class="bar" style="width:0%;"></div> </div>
</div>
</div>
</div>
<!-- Upload Section -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-cloud-upload text-primary"></i> Upload Completed Forms
</h4>
</div>
<div class="card-body p-4">
<%= form_for @benefits, url: upload_path, html: { multipart: true, id: "fi", class: "needs-validation" } do |f| %>
<%= hidden_field "benefits", "backup", value: false %>
<div class="row g-3">
<div class="col-12">
<div class="upload-area p-4 text-center border rounded" style="border: 2px dashed #dee2e6; background: var(--rg-light); transition: all 0.3s;">
<i class="bi bi-cloud-arrow-up" style="font-size: 3rem; color: var(--rg-secondary);"></i>
<h5 class="mt-3 mb-2">Select File to Upload</h5>
<p class="text-muted mb-3">Choose a file from your computer</p>
<div class="file-input-wrapper">
<label for="benefits_upload" class="btn btn-primary mb-3" style="cursor: pointer;">
<i class="bi bi-folder2-open"></i> Choose File
</label>
<%= f.file_field :upload, class: "d-none", id: "benefits_upload" %>
</div>
<div class="selected-file mt-3">
<span class="filename text-muted">
<i class="bi bi-file-earmark"></i> No file selected
</span>
</div> </div>
</div> </div>
</div> </div>
<!-- The loading indicator is shown during image processing -->
<div class="fileupload-loading"></div>
<br>
<!-- The table listing the files available for upload/download -->
<table class="table table-striped"><tbody class="files" data-toggle="modal-gallery" data-target="#modal-gallery"></tbody>
</table>
<% end %>
<div id="progress">
<div class="bar" style="width: 0%;"></div>
</div>
</div> <div class="col-12">
<div class="d-flex gap-2">
<button id="start_upload" type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-upload"></i> Upload File
</button>
<button type="button" class="btn btn-outline-secondary btn-lg" onclick="document.getElementById('fi').reset(); $('.filename').html('<i class=\'bi bi-file-earmark\'></i> No file selected');">
<i class="bi bi-x-circle"></i> Cancel
</button>
</div>
</div>
<div class="col-12">
<!-- Progress Bar -->
<div class="progress" style="height: 25px; display: none;" id="upload-progress">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" role="progressbar" style="width: 0%;" id="progress-bar">
<span id="progress-text">0%</span>
</div>
</div>
</div>
<div class="col-12">
<!-- Files Table -->
<table class="table table-hover d-none" id="files-table">
<thead class="table-light">
<tr>
<th>File Name</th>
<th>Size</th>
<th>Status</th>
</tr>
</thead>
<tbody class="files" data-toggle="modal-gallery" data-target="#modal-gallery"></tbody>
</table>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- Info Box -->
<div class="row mt-4">
<div class="col-12">
<div class="alert alert-info" role="alert">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-2">Important Information</h5>
<ul class="mb-0 ps-3">
<li>Download benefit forms, fill them out completely, and upload them back</li>
<li>Accepted file formats: PDF, DOC, DOCX, JPG, PNG</li>
<li>Maximum file size: 10MB</li>
<li>All uploaded documents are securely stored</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
@@ -111,16 +140,91 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
$(function() { function makeActive() {
$("#benefits_upload").change(function (){ $('li[id="benefit_forms"]').addClass('active');
var fileName = $(this).val(); }
$(".filename").html(fileName);
}); $(document).ready(function() {
makeActive();
// File input change handler
$("#benefits_upload").change(function() {
var fileName = $(this).val();
if (fileName) {
// Extract just the filename from the full path
fileName = fileName.split('\\').pop().split('/').pop();
$(".filename").html('<i class="bi bi-file-earmark-check-fill text-success"></i> ' + fileName);
// Highlight the upload area
$(".upload-area").css({
'border-color': 'var(--rg-success)',
'background': 'rgba(6, 214, 160, 0.05)'
});
} else {
$(".filename").html('<i class="bi bi-file-earmark"></i> No file selected');
$(".upload-area").css({
'border-color': '#dee2e6',
'background': 'var(--rg-light)'
});
}
}); });
function makeActive(){ // Form submission handler (for demonstration)
$('li[id="benefit_forms"]').addClass('active'); $("#fi").submit(function(e) {
}; var fileName = $("#benefits_upload").val();
if (!fileName) {
e.preventDefault();
alert('Please select a file to upload');
return false;
}
$(document).ready(makeActive); // Show progress bar
</script> $("#upload-progress").show();
// Simulate upload progress (in real implementation, this would be handled by the server)
var progress = 0;
var interval = setInterval(function() {
progress += 10;
$("#progress-bar").css('width', progress + '%');
$("#progress-text").text(progress + '%');
if (progress >= 100) {
clearInterval(interval);
}
}, 200);
});
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
</script>
<style>
.hover-card {
transition: all 0.3s ease;
}
.hover-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15) !important;
}
.upload-area:hover {
border-color: var(--rg-primary) !important;
background: rgba(230, 57, 70, 0.03) !important;
}
.file-input-wrapper input[type="file"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
#progress-bar {
font-weight: 600;
line-height: 25px;
}
</style>
+93 -53
View File
@@ -1,54 +1,94 @@
<div id="column_chart"></div> <div class="p-4">
<!-- Google Visualization JS --> <div class="table-responsive">
<script type="text/javascript" src="https://www.google.com/jsapi"></script> <table class="table table-hover">
<thead class="table-light">
<tr>
<th>Year</th>
<th class="text-end">Visitors</th>
<th class="text-end">Orders</th>
<th class="text-end">Income</th>
<th class="text-end">Expenses</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>2007</strong></td>
<td class="text-end">300</td>
<td class="text-end">800</td>
<td class="text-end">900</td>
<td class="text-end">300</td>
</tr>
<tr>
<td><strong>2008</strong></td>
<td class="text-end">1,170</td>
<td class="text-end">860</td>
<td class="text-end">1,220</td>
<td class="text-end">564</td>
</tr>
<tr>
<td><strong>2009</strong></td>
<td class="text-end">260</td>
<td class="text-end">1,120</td>
<td class="text-end">2,870</td>
<td class="text-end">2,340</td>
</tr>
<tr>
<td><strong>2010</strong></td>
<td class="text-end">1,030</td>
<td class="text-end">540</td>
<td class="text-end">3,430</td>
<td class="text-end">1,200</td>
</tr>
<tr>
<td><strong>2011</strong></td>
<td class="text-end">200</td>
<td class="text-end">700</td>
<td class="text-end">1,700</td>
<td class="text-end">770</td>
</tr>
<tr>
<td><strong>2012</strong></td>
<td class="text-end">1,170</td>
<td class="text-end">2,160</td>
<td class="text-end">3,920</td>
<td class="text-end">800</td>
</tr>
</tbody>
</table>
</div>
<script type="text/javascript"> <div class="row mt-4 g-3">
// google.load("visualization", "1", {packages:["corechart"]}); <div class="col-md-3">
<div class="card text-center" style="border-left: 4px solid #b5799e;">
<div class="card-body">
<div class="text-muted small mb-1">Total Visitors</div>
function drawChart3() { <h3 class="mb-0" style="color: #b5799e;">3,130</h3>
var data = google.visualization.arrayToDataTable([ </div>
['Year', 'Visitors', 'Orders', 'Income', 'Expenses'], </div>
['2007', 300, 800, 900, 300], </div>
['2008', 1170, 860, 1220, 564], <div class="col-md-3">
['2009', 260, 1120, 2870, 2340], <div class="card text-center" style="border-left: 4px solid #579da9;">
['2010', 1030, 540, 3430, 1200], <div class="card-body">
['2011', 200, 700, 1700, 770], <div class="text-muted small mb-1">Total Orders</div>
['2012', 1170, 2160, 3920, 800], ]); <h3 class="mb-0" style="color: #579da9;">6,180</h3>
</div>
var options = { </div>
width: 'auto', </div>
height: '160', <div class="col-md-3">
backgroundColor: 'transparent', <div class="card text-center" style="border-left: 4px solid #e26666;">
colors: ['#b5799e', '#579da9', '#e26666', '#1e825e', '#dba26b'], <div class="card-body">
tooltip: { <div class="text-muted small mb-1">Total Income</div>
textStyle: { <h3 class="mb-0" style="color: #e26666;">14,040</h3>
color: '#666666', </div>
fontSize: 11 </div>
}, </div>
showColorCode: true <div class="col-md-3">
}, <div class="card text-center" style="border-left: 4px solid #1e825e;">
legend: { <div class="card-body">
textStyle: { <div class="text-muted small mb-1">Total Expenses</div>
color: 'black', <h3 class="mb-0" style="color: #1e825e;">5,174</h3>
fontSize: 12 </div>
} </div>
}, </div>
chartArea: { </div>
left: 60, </div>
top: 10,
height: '80%'
},
};
var chart = new google.visualization.ColumnChart(document.getElementById('column_chart'));
chart.draw(data, options);
}
$(document).ready(
drawChart3()
);
</script>
+76 -42
View File
@@ -1,56 +1,90 @@
<div class="dashboard-wrapper"> <div class="container-fluid">
<div class="main-container"> <div class="row">
<div class="row-fluid"> <div class="col-12">
<div class="span12"> <!--begin span12 --> <div class="card shadow-sm">
<div class="widget"> <div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
<div class="widget-header"> <h4 class="mb-0">
<div class="title"> <i class="bi bi-graph-up text-primary"></i> Current Statistics
<span class="fs1" aria-hidden="true" data-icon="&#xe0a0;"></span> Current Statistics </h4>
</div> <!-- Chart Type Toggle -->
<!-- Begin Title Buttons--> <div class="btn-group" role="group" aria-label="Chart type selection">
<div class="tools pull-right"> <button id="change_to_bar_graph" class="btn btn-outline-primary btn-sm" title="Bar Graph View" aria-label="Switch to bar graph">
<div class="btn-group"> <i class="bi bi-bar-chart-fill"></i> Bar Graph
<button id="change_to_bar_graph" class="btn btn-small"> </button>
<span aria-label="change to bar graph" data-icon="&#xe14b;"></span> <button id="change_to_pie_charts" class="btn btn-outline-primary btn-sm" title="Pie Charts View" aria-label="Switch to pie charts">
</button> <i class="bi bi-pie-chart-fill"></i> Pie Charts
<button id="change_to_pie_charts" class="btn btn-small"> </button>
<span aria-label="change to pie charts" data-icon="&#xe096;"></span>
</button>
</div>
</div>
<!-- End Title Buttons-->
</div>
<div id="charts_body" class="widget-body">
<%#= render partial: "dashboard_stats" %>
</div>
<div class="clearfix">
</div> </div>
</div> </div>
</div> <!-- end span12 --> <div id="charts_body" class="card-body p-4">
<!-- Charts will load here dynamically -->
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading charts...</span>
</div>
<p class="text-muted mt-3">Loading statistics...</p>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
function makeActive(){ function makeActive() {
$('li[id="home"]').addClass('active'); $('li[id="home"]').addClass('active');
}; }
$("#change_to_bar_graph").click(function(event) { $("#change_to_bar_graph").click(function(event) {
event.preventDefault(); event.preventDefault();
$("#charts_body").empty()
$("#charts_body").load(<%= sanitize change_graph_dashboard_index_path(:graph => "bar_graph").inspect %>); // Add loading state
$("#charts_body").html('<div class="text-center py-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div><p class="text-muted mt-3">Loading bar graph...</p></div>');
})
// Remove active state from other button
$("#change_to_pie_charts").removeClass('active');
$(this).addClass('active');
// Load new content
$("#charts_body").load(<%= sanitize change_graph_dashboard_index_path(:graph => "bar_graph").inspect %>);
});
$("#change_to_pie_charts").click(function(event) { $("#change_to_pie_charts").click(function(event) {
event.preventDefault(); event.preventDefault();
$("#charts_body").empty()
$("#charts_body").load(<%= sanitize change_graph_dashboard_index_path(:graph => "pie_charts").inspect %>);
})
$(document).ready( // Add loading state
makeActive, $("#charts_body").html('<div class="text-center py-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div><p class="text-muted mt-3">Loading pie charts...</p></div>');
$("#charts_body").load(<%= sanitize change_graph_dashboard_index_path(:graph => "pie_charts").inspect %>)
); // Remove active state from other button
$("#change_to_bar_graph").removeClass('active');
$(this).addClass('active');
// Load new content
$("#charts_body").load(<%= sanitize change_graph_dashboard_index_path(:graph => "pie_charts").inspect %>);
});
$(document).ready(function() {
makeActive();
// Mark pie charts as default active view
$("#change_to_pie_charts").addClass('active');
// Load default view
$("#charts_body").load(<%= sanitize change_graph_dashboard_index_path(:graph => "pie_charts").inspect %>);
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
</script> </script>
<style>
/* Active button state for chart toggles */
#change_to_bar_graph.active,
#change_to_pie_charts.active {
background-color: var(--rg-primary);
color: white;
border-color: var(--rg-primary);
}
</style>
+154 -78
View File
@@ -48,8 +48,18 @@
<% end %> <% end %>
<script type="text/javascript"> <script type="text/javascript">
// Store timeout IDs so we can clear them on page navigation
var chartTimeouts = [];
function pieChartHome() { function pieChartHome() {
// Clear any existing timeouts first
chartTimeouts.forEach(function(id) { clearTimeout(id); });
chartTimeouts = [];
$(function () { $(function () {
// Only initialize if chart elements exist
if ($('.chart1').length === 0) return;
//create instance //create instance
$('.chart1').easyPieChart({ $('.chart1').easyPieChart({
animate: 2000, animate: 2000,
@@ -60,24 +70,37 @@ function pieChartHome() {
lineWidth: 7, lineWidth: 7,
}); });
//update instance after 5 sec //update instance after 5 sec
setTimeout(function () { chartTimeouts.push(setTimeout(function () {
$('.chart1').data('easyPieChart').update(50); if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
}, 5000); $('.chart1').data('easyPieChart').update(50);
setTimeout(function () { }
$('.chart1').data('easyPieChart').update(70); }, 5000));
}, 10000); chartTimeouts.push(setTimeout(function () {
setTimeout(function () { if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(30); $('.chart1').data('easyPieChart').update(70);
}, 15000); }
setTimeout(function () { }, 10000));
$('.chart1').data('easyPieChart').update(90); chartTimeouts.push(setTimeout(function () {
}, 19000); if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
setTimeout(function () { $('.chart1').data('easyPieChart').update(30);
$('.chart1').data('easyPieChart').update(40); }
}, 32000); }, 15000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(90);
}
}, 19000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(40);
}
}, 32000));
}); });
$(function () { $(function () {
// Only initialize if chart elements exist
if ($('.chart2').length === 0) return;
//create instance //create instance
$('.chart2').easyPieChart({ $('.chart2').easyPieChart({
animate: 2000, animate: 2000,
@@ -88,24 +111,37 @@ function pieChartHome() {
lineWidth: 7, lineWidth: 7,
}); });
//update instance after 5 sec //update instance after 5 sec
setTimeout(function () { chartTimeouts.push(setTimeout(function () {
$('.chart2').data('easyPieChart').update(90); if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
}, 10000); $('.chart2').data('easyPieChart').update(90);
setTimeout(function () { }
$('.chart2').data('easyPieChart').update(40); }, 10000));
}, 18000); chartTimeouts.push(setTimeout(function () {
setTimeout(function () { if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(70); $('.chart2').data('easyPieChart').update(40);
}, 28000); }
setTimeout(function () { }, 18000));
$('.chart2').data('easyPieChart').update(50); chartTimeouts.push(setTimeout(function () {
}, 32000); if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
setTimeout(function () { $('.chart2').data('easyPieChart').update(70);
$('.chart2').data('easyPieChart').update(80); }
}, 40000); }, 28000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(50);
}
}, 32000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(80);
}
}, 40000));
}); });
$(function () { $(function () {
// Only initialize if chart elements exist
if ($('.chart3').length === 0) return;
//create instance //create instance
$('.chart3').easyPieChart({ $('.chart3').easyPieChart({
animate: 2000, animate: 2000,
@@ -116,24 +152,37 @@ function pieChartHome() {
lineWidth: 7, lineWidth: 7,
}); });
//update instance after 5 sec //update instance after 5 sec
setTimeout(function () { chartTimeouts.push(setTimeout(function () {
$('.chart3').data('easyPieChart').update(20); if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
}, 9000); $('.chart3').data('easyPieChart').update(20);
setTimeout(function () { }
$('.chart3').data('easyPieChart').update(59); }, 9000));
}, 20000); chartTimeouts.push(setTimeout(function () {
setTimeout(function () { if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(38); $('.chart3').data('easyPieChart').update(59);
}, 35000); }
setTimeout(function () { }, 20000));
$('.chart3').data('easyPieChart').update(79); chartTimeouts.push(setTimeout(function () {
}, 49000); if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
setTimeout(function () { $('.chart3').data('easyPieChart').update(38);
$('.chart3').data('easyPieChart').update(96); }
}, 52000); }, 35000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(79);
}
}, 49000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(96);
}
}, 52000));
}); });
$(function () { $(function () {
// Only initialize if chart elements exist
if ($('.chart4').length === 0) return;
//create instance //create instance
$('.chart4').easyPieChart({ $('.chart4').easyPieChart({
animate: 2000, animate: 2000,
@@ -144,24 +193,37 @@ function pieChartHome() {
lineWidth: 7, lineWidth: 7,
}); });
//update instance after 5 sec //update instance after 5 sec
setTimeout(function () { chartTimeouts.push(setTimeout(function () {
$('.chart4').data('easyPieChart').update(40); if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
}, 6000); $('.chart4').data('easyPieChart').update(40);
setTimeout(function () { }
$('.chart4').data('easyPieChart').update(67); }, 6000));
}, 14000); chartTimeouts.push(setTimeout(function () {
setTimeout(function () { if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(43); $('.chart4').data('easyPieChart').update(67);
}, 23000); }
setTimeout(function () { }, 14000));
$('.chart4').data('easyPieChart').update(80); chartTimeouts.push(setTimeout(function () {
}, 36000); if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
setTimeout(function () { $('.chart4').data('easyPieChart').update(43);
$('.chart4').data('easyPieChart').update(66); }
}, 41000); }, 23000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(80);
}
}, 36000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(66);
}
}, 41000));
}); });
$(function () { $(function () {
// Only initialize if chart elements exist
if ($('.chart5').length === 0) return;
//create instance //create instance
$('.chart5').easyPieChart({ $('.chart5').easyPieChart({
animate: 3000, animate: 3000,
@@ -172,28 +234,42 @@ function pieChartHome() {
lineWidth: 7, lineWidth: 7,
}); });
//update instance after 5 sec //update instance after 5 sec
setTimeout(function () { chartTimeouts.push(setTimeout(function () {
$('.chart5').data('easyPieChart').update(30); if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
}, 9000); $('.chart5').data('easyPieChart').update(30);
setTimeout(function () { }
$('.chart5').data('easyPieChart').update(87); }, 9000));
}, 19000); chartTimeouts.push(setTimeout(function () {
setTimeout(function () { if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(28); $('.chart5').data('easyPieChart').update(87);
}, 27000); }
setTimeout(function () { }, 19000));
$('.chart5').data('easyPieChart').update(69); chartTimeouts.push(setTimeout(function () {
}, 39000); if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
setTimeout(function () { $('.chart5').data('easyPieChart').update(28);
$('.chart5').data('easyPieChart').update(99); }
}, 47000); }, 27000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(69);
}
}, 39000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(99);
}
}, 47000));
}); });
} }
$(document).ready( $(document).ready(pieChartHome);
pieChartHome()
); // Clear timeouts when navigating away with Turbolinks
$(document).on('turbolinks:before-visit', function() {
chartTimeouts.forEach(function(id) { clearTimeout(id); });
chartTimeouts = [];
});
</script> </script>
+420 -25
View File
@@ -1,43 +1,438 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en" data-bs-theme="light">
<head> <head>
<title>RailsGoat</title> <meta charset="utf-8">
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> <meta name="viewport" content="width=device-width, initial-scale=1">
<%= javascript_include_tag "application", "data-turbolinks-track" => true %> <title>RailsGoat - OWASP Security Training</title>
<%#= csrf_meta_tags %> <!-- <~ What is this for? I hear it helps w/ JS and Sea-surfing.....whatevz --> <%#= csrf_meta_tags %> <!-- <~ What is this for? I hear it helps w/ JS and Sea-surfing.....whatevz -->
<!-- bootstrap css -->
<!-- VULNERABILITY A03:2025 - Software Supply Chain Failures
Missing Subresource Integrity (SRI) checks on CDN assets
If the CDN is compromised, malicious code can be injected without detection
SECURE: Should include integrity="sha384-..." crossorigin="anonymous"
See: /tutorials/supply_chain for exploitation details
-->
<!-- Load jQuery FIRST - other scripts depend on it -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<!-- Bootstrap CSS and Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<!-- Rails assets - loaded AFTER jQuery -->
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => "reload" %>
<%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %>
<!-- Bootstrap JS - loaded last -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- FullCalendar and dependencies for PTO page -->
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@3.10.5/dist/fullcalendar.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@3.10.5/dist/fullcalendar.min.js"></script>
<!-- Modern Design System -->
<style>
:root {
--rg-primary: #e63946;
--rg-primary-dark: #d62828;
--rg-secondary: #457b9d;
--rg-secondary-dark: #1d3557;
--rg-success: #06d6a0;
--rg-warning: #ffb703;
--rg-danger: #e63946;
--rg-light: #f8f9fa;
--rg-dark: #1d3557;
--rg-sidebar-width: 250px;
--rg-header-height: 60px;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--rg-light);
min-height: 100vh;
}
/* Modern Header */
.rg-header {
background: white;
border-bottom: none;
height: var(--rg-header-height);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1030;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
overflow: visible;
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.95);
}
.rg-header .container-fluid {
padding-left: 2rem;
padding-right: 2rem;
}
.rg-header .col-auto:first-child {
padding-left: 0;
margin-left: 0;
}
.rg-brand {
font-size: 1.5rem;
font-weight: 700;
color: var(--rg-primary);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
line-height: 1;
}
.rg-brand:hover {
color: var(--rg-primary-dark);
}
.rg-brand i {
font-size: 1.75rem;
line-height: 1;
display: inline-block;
min-width: 1.75rem;
text-align: center;
}
.rg-header .row {
margin-left: 0;
margin-right: 0;
}
/* Modern Sidebar */
.rg-sidebar {
position: fixed;
top: var(--rg-header-height);
left: 0;
bottom: 0;
width: var(--rg-sidebar-width);
background: var(--rg-dark);
color: white;
overflow-y: auto;
transition: transform 0.3s ease;
box-shadow: 2px 0 12px rgba(0,0,0,0.1);
}
.rg-sidebar-nav {
list-style: none;
padding: 1rem 0;
margin: 0;
}
.rg-sidebar-nav li a {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
margin: 0.25rem 0.75rem;
color: rgba(255,255,255,0.8);
text-decoration: none;
transition: all 0.2s;
border-radius: 0.75rem;
}
.rg-sidebar-nav li a:hover,
.rg-sidebar-nav li a.active {
background: rgba(255,255,255,0.15);
color: white;
transform: translateX(4px);
}
.rg-sidebar-nav li a i {
font-size: 1.25rem;
margin-right: 0.75rem;
width: 24px;
text-align: center;
}
/* Main Content */
.rg-main {
margin-top: var(--rg-header-height);
margin-left: var(--rg-sidebar-width);
padding: 2rem;
min-height: calc(100vh - var(--rg-header-height));
}
.rg-main.no-sidebar {
margin-left: 0;
}
/* Modern Cards */
.rg-card, .card {
background: white;
border-radius: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 1.5rem;
border: none;
overflow: hidden;
}
.rg-card:hover, .card:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
transition: box-shadow 0.3s ease;
}
/* Modern Buttons */
.btn {
border-radius: 0.75rem;
padding: 0.5rem 1.25rem;
font-weight: 500;
transition: all 0.2s ease;
border: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.btn-sm {
border-radius: 0.625rem;
padding: 0.375rem 1rem;
font-size: 0.875rem;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.btn-primary {
background: linear-gradient(135deg, var(--rg-primary) 0%, var(--rg-primary-dark) 100%);
border-color: transparent;
color: white;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--rg-primary-dark) 0%, #c11f2b 100%);
border-color: transparent;
color: white;
}
.btn-outline-primary {
border: 2px solid var(--rg-primary);
color: var(--rg-primary);
background: white;
}
.btn-outline-primary:hover {
background: var(--rg-primary);
color: white;
border-color: var(--rg-primary);
}
.btn-warning {
background: linear-gradient(135deg, #ffb703 0%, #fb8500 100%);
color: white;
border: none;
}
.btn-warning:hover {
background: linear-gradient(135deg, #fb8500 0%, #e85d04 100%);
color: white;
}
.btn-outline-secondary {
border: 2px solid #dee2e6;
color: #6c757d;
background: white;
}
.btn-outline-secondary:hover {
background: #f8f9fa;
border-color: #adb5bd;
color: #495057;
}
/* Modern Alerts */
.alert {
border-radius: 0.875rem;
border: none;
padding: 1rem 1.25rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
/* Modern Tables */
.table {
border-radius: 0.75rem;
overflow: hidden;
}
.table thead th {
background: var(--rg-light);
font-weight: 600;
border-bottom: 2px solid #dee2e6;
padding: 1rem;
}
.table tbody tr {
transition: background-color 0.2s ease;
}
.table tbody tr:hover {
background-color: rgba(230, 57, 70, 0.03);
}
/* Modern Badges */
.badge {
border-radius: 0.5rem;
padding: 0.35rem 0.65rem;
font-weight: 500;
}
/* Modern Dropdowns */
.dropdown-menu {
border-radius: 0.75rem;
border: none;
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
padding: 0.5rem;
}
.dropdown-item {
border-radius: 0.5rem;
padding: 0.5rem 1rem;
transition: all 0.2s ease;
}
.dropdown-item:hover {
background-color: rgba(230, 57, 70, 0.08);
transform: translateX(2px);
}
/* Modern Widget Styling */
.widget {
background: white;
border-radius: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 1.5rem;
border: none;
overflow: hidden;
}
.widget-header {
background: var(--rg-light);
border-bottom: none;
padding: 1.25rem 1.5rem;
border-radius: 1rem 1rem 0 0;
}
.widget-header .title {
font-weight: 600;
color: var(--rg-dark);
font-size: 1.1rem;
}
.widget-body {
padding: 1.5rem;
}
/* Modern Login Page */
.rg-login-wrapper {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--rg-secondary-dark) 0%, var(--rg-secondary) 100%);
position: relative;
}
.rg-login-wrapper::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 20% 50%, rgba(230, 57, 70, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(69, 123, 157, 0.1) 0%, transparent 50%);
}
.rg-login-card {
background: white;
border-radius: 1.5rem;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 3rem;
width: 100%;
max-width: 450px;
position: relative;
z-index: 1;
}
.form-control, .form-select {
border-radius: 0.75rem;
border: 2px solid #e9ecef;
padding: 0.75rem 1rem;
transition: all 0.2s ease;
}
.form-control:focus, .form-select:focus {
border-color: var(--rg-primary);
box-shadow: 0 0 0 3px rgba(230, 57, 70, 0.1);
}
.input-group-text {
border-radius: 0.75rem 0 0 0.75rem;
border: 2px solid #e9ecef;
border-right: none;
background: #f8f9fa;
}
.input-group .form-control {
border-radius: 0 0.75rem 0.75rem 0;
}
.rg-login-header {
text-align: center;
margin-bottom: 2rem;
}
.rg-login-logo {
font-size: 3rem;
color: var(--rg-primary);
margin-bottom: 1rem;
}
/* Responsive */
@media (max-width: 768px) {
.rg-sidebar {
transform: translateX(-100%);
}
.rg-sidebar.show {
transform: translateX(0);
}
.rg-main {
margin-left: 0;
}
}
/* VULNERABILITY: XSS via cookie font-size */
<% <%
if cookies[:font] if cookies[:font]
%> %>
<style>body { font-size:<%= raw cookies[:font] %> !important;}</style> body { font-size:<%= raw cookies[:font] %> !important;}
<% <%
end end
%> %>
</style>
</head> </head>
<body> <body>
<%= render "layouts/shared/header" %> <%= render "layouts/shared/header" %>
<%= render "layouts/shared/sidebar" %> <%= render "layouts/shared/sidebar" %>
<div class="container-fluid">
<% if current_user %> <main class="rg-main <%= 'no-sidebar' unless current_user %>">
<div class="dashboard-wrapper">
<%= render "layouts/shared/messages" %>
<%= yield %>
</div>
<% else %>
<div class="login-wrapper">
<%= render "layouts/shared/messages" %> <%= render "layouts/shared/messages" %>
<%= yield %> <%= yield %>
</div> </main>
<% end %>
</div>
<%= render "layouts/shared/footer" %> <%= render "layouts/shared/footer" %>
<script type="text/javascript">
//Dropdown
$('.dropdown-toggle').dropdown();
</script>
</body> </body>
</html> </html>
+54 -20
View File
@@ -1,22 +1,56 @@
<footer> <% if current_user %>
<p align="center"> <footer class="border-top mt-5 py-4 text-center text-muted bg-white">
&copy; The Open Worldwide Application Security Project - OWASP, 2015 <div class="container">
</p> <div class="row">
</footer> <div class="col-md-12">
<p class="mb-1">
<i class="bi bi-shield-check"></i>
&copy; <%= Date.current.year %> The Open Worldwide Application Security Project - OWASP
</p>
<p class="small mb-0">
<a href="https://owasp.org" target="_blank" class="text-decoration-none me-3">
<i class="bi bi-globe"></i> OWASP.org
</a>
<a href="https://github.com/OWASP/railsgoat" target="_blank" class="text-decoration-none me-3">
<i class="bi bi-github"></i> GitHub
</a>
<a href="https://github.com/OWASP/railsgoat/wiki" target="_blank" class="text-decoration-none">
<i class="bi bi-book"></i> Documentation
</a>
</p>
</div>
</div>
</div>
</footer>
<% end %>
<script type="text/javascript"> <!-- Scroll to Top Button -->
<button id="scrollTopBtn" class="btn btn-primary rounded-circle position-fixed bottom-0 end-0 m-4" style="width: 48px; height: 48px; display: none; z-index: 1000;" title="Scroll to top">
<i class="bi bi-arrow-up"></i>
</button>
//ScrollUp <script>
$(function () { // Modern scroll-to-top without jQuery
$.scrollUp({ (function() {
scrollName: 'scrollUp', // Element ID const scrollBtn = document.getElementById('scrollTopBtn');
topDistance: '300', // Distance from top before showing element (px)
topSpeed: 300, // Speed back to top (ms) if (scrollBtn) {
animation: 'fade', // Fade, slide, none // Show/hide button based on scroll position
animationInSpeed: 400, // Animation in speed (ms) window.addEventListener('scroll', function() {
animationOutSpeed: 400, // Animation out speed (ms) if (window.pageYOffset > 300) {
scrollText: 'Scroll to top', // Text for element scrollBtn.style.display = 'block';
activeOverlay: false, // Set CSS color to display scrollUp active point, e.g '#00FFFF' } else {
}); scrollBtn.style.display = 'none';
}); }
</script> });
// Scroll to top on click
scrollBtn.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
})();
</script>
+108 -81
View File
@@ -1,88 +1,115 @@
<% if current_user %> <% if current_user %>
<header> <!-- Authenticated Header -->
<header class="rg-header">
<span style="color:#eee;margin-left:10px;"> <div class="container-fluid h-100">
Font Size: <div class="row h-100 align-items-center">
<a data-no-turbolink='true' href="/dashboard/home?font=8pt" style="font-size:10pt;color:#eee" aria-label="small font">A</a> <div class="col-auto">
<a data-no-turbolink='true' href="/dashboard/home?font=200%25" style="font-size:18pt;color:#eee;" aria-label="large font">A</a> <a href="<%= home_dashboard_index_path %>" class="rg-brand">
</span> <i class="bi bi-shield-fill-exclamation"></i> RailsGoat
</a>
<div class="user-profile"> </div>
<button data-toggle="dropdown" class="dropdown-toggle">
<img src=" <%= image_path('profile_color.jpg')%>" alt="profile"> <div class="col"></div>
</button>
<span class="caret"></span> <div class="col-auto">
<ul class="dropdown-menu pull-right"> <div class="d-flex align-items-center gap-3">
<li> <!-- Font Size Controls -->
<%= link_to "Account settings", user_account_settings_path(user_id: current_user.id) %> <div class="btn-group btn-group-sm" role="group" aria-label="Font size controls">
</li> <a href="/dashboard/home?font=8pt" class="btn btn-outline-secondary" style="font-size: 10pt;" title="Small font" aria-label="Small font">
<li> <i class="bi bi-type"></i>
<%= link_to "Logout", logout_path %> </a>
</li> <a href="/dashboard/home?font=200%25" class="btn btn-outline-secondary" style="font-size: 14pt;" title="Large font" aria-label="Large font">
</ul> <i class="bi bi-type"></i>
</a>
</div>
<!-- Tutorial Link -->
<%= button_to "https://github.com/OWASP/railsgoat/wiki", {
method: "get",
class: "btn btn-sm btn-outline-primary",
onclick: "window.open('https://github.com/OWASP/railsgoat/wiki', '_blank'); return false;"
} do %>
<i class="bi bi-book"></i> Tutorials
<% end %>
<!-- User Dropdown -->
<div class="dropdown">
<button class="btn btn-link text-decoration-none dropdown-toggle d-flex align-items-center gap-2" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center" style="width: 32px; height: 32px;">
<i class="bi bi-person-fill text-white"></i>
</div>
<!--
VULNERABILITY: XSS via html_safe
I'm going to use HTML safe because we had some weird stuff
going on with funny chars and jquery, plus it says safe so I'm guessing
nothing bad will happen
-->
<span class="text-dark"><%= current_user.first_name.html_safe %></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<%= link_to user_account_settings_path(user_id: current_user.id), class: "dropdown-item" do %>
<i class="bi bi-gear"></i> Account Settings
<% end %>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<%= link_to logout_path, class: "dropdown-item text-danger" do %>
<i class="bi bi-box-arrow-right"></i> Logout
<% end %>
</li>
</ul>
</div>
</div>
</div>
</div> </div>
<ul class="mini-nav"> </div>
<li style="color: #FFFFFF">
<!--
I'm going to use HTML safe because we had some weird stuff
going on with funny chars and jquery, plus it says safe so I'm guessing
nothing bad will happen
-->
Welcome, <%= current_user.first_name.html_safe %>
</li>
</ul>
<ul class="mini-nav">
<li>
<%= button_to "Visit Tutorial", nil,
{
:class => "btn",
:method => "get",
:onclick => "window.open('https://github.com/OWASP/railsgoat/wiki', '_blank')"
} %>
</li>
</ul>
</header> </header>
<% else %> <% else %>
<!-- Want to use this template whether auth'd or not so I've got some code to determine how to render below --> <!-- Unauthenticated Header -->
<header> <header class="rg-header">
<ul class="mini-nav"> <div class="container-fluid h-100">
<li> <div class="row h-100 align-items-center">
<%= button_to "Signup", signup_path, {:class => "btn btn-primary", :method => "get"} %> <div class="col-auto">
</li> <a href="<%= login_path %>" class="rg-brand">
</ul> <i class="bi bi-shield-fill-exclamation"></i> RailsGoat
<ul class="mini-nav"> </a>
<li> </div>
<%= button_to "login", login_path, {:class => "btn", :method => "get"} %>
</li> <div class="col"></div>
</ul>
<ul class="mini-nav"> <div class="col-auto">
<li> <div class="d-flex align-items-center gap-2">
<%= button_to "Tutorial Credentials", "#myModalLabel1", {:id => "show_creds_btn", :class => "btn btn-danger", :method => "get"} %> <%= link_to credentials_tutorials_path, class: "btn btn-sm btn-warning" do %>
</li> <i class="bi bi-key"></i> Demo Credentials
</ul> <% end %>
<ul class="mini-nav">
<li> <%= button_to "https://github.com/OWASP/railsgoat/wiki", {
<%= button_to "Visit Tutorial", nil, method: "get",
{ class: "btn btn-sm btn-outline-primary",
:class => "btn", onclick: "window.open('https://github.com/OWASP/railsgoat/wiki', '_blank'); return false;"
:method => "get", } do %>
:onclick => "window.open('https://github.com/OWASP/railsgoat/wiki', '_blank')" <i class="bi bi-book"></i> Tutorials
} %> <% end %>
</li>
</ul> <%= button_to signup_path, {
class: "btn btn-sm btn-primary",
method: "get"
} do %>
<i class="bi bi-person-plus"></i> Sign Up
<% end %>
<%= button_to login_path, {
class: "btn btn-sm btn-outline-primary",
method: "get"
} do %>
<i class="bi bi-box-arrow-in-right"></i> Login
<% end %>
</div>
</div>
</div>
</div>
</header> </header>
<div id="modal_div" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myAlert" aria-hidden="true">
</div>
<script type="text/javascript">
$('#show_creds_btn').click(function(event) {
event.preventDefault();
$("#modal_div").load(<%= credentials_tutorials_path.inspect.html_safe %>);
$("#modal_div").modal("show");
});
</script>
<% end %> <% end %>
+35 -16
View File
@@ -1,19 +1,38 @@
<% flash.each do |name, msg| %> <% flash.each do |name, msg| %>
<% name = name.to_sym %> <% name = name.to_sym %>
<% if name == :error %> <%
<div class="alert alert-error"> alert_class = case name
<a class="close" aria-label="dismiss" data-dismiss="alert" href="#">×</a> when :error, :alert
<%= content_tag :div, msg, :id => "flash_notice" %> 'alert-danger'
when :success, :notice
'alert-success'
when :info
'alert-info'
when :warning
'alert-warning'
else
'alert-secondary'
end
icon_class = case name
when :error, :alert
'bi-exclamation-circle-fill'
when :success, :notice
'bi-check-circle-fill'
when :info
'bi-info-circle-fill'
when :warning
'bi-exclamation-triangle-fill'
else
'bi-bell-fill'
end
%>
<div class="alert <%= alert_class %> alert-dismissible fade show d-flex align-items-center" role="alert">
<i class="bi <%= icon_class %> me-2"></i>
<div class="flex-grow-1">
<%= msg %>
</div> </div>
<% elsif name == :success %> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<div class="alert alert-success"> </div>
<a class="close" aria-label="dismiss" data-dismiss="alert" href="#">×</a> <% end %>
<%= content_tag :div, msg, :id => "flash_notice" %>
</div>
<% elsif name == :info %>
<div class="alert alert-info">
<a class="close" aria-label="dismiss" data-dismiss="alert" href="#">×</a>
<%= content_tag :div, msg, :id => "flash_notice" %>
</div>
<% end %>
<% end %>
+83 -135
View File
@@ -1,142 +1,90 @@
<% if current_user %> <% if current_user %>
<div id="mainnav" class="hidden-phone hidden-tablet"> <nav class="rg-sidebar">
<ul style="display: block;"> <ul class="rg-sidebar-nav">
<li id="home"> <li>
<%= link_to home_dashboard_index_path do %> <%= link_to home_dashboard_index_path, class: "#{controller_name == 'dashboard' ? 'active' : ''}" do %>
<div class="icon"> <i class="bi bi-speedometer2"></i>
<span class="fs1" aria-hidden="true" data-icon="&#xe0a0;"></span> <span>Dashboard</span>
</div> <% end %>
Dashboard </li>
<% end %>
</li>
<% if is_admin? %>
<li class="submenu" id='admin'>
<a href="#">
<div class="icon">
<span class="fs1" aria-hidden="true" data-icon="&#xe1c8;"></span>
</div>
Admin
</a>
<ul>
<li>
<%= link_to admin_dashboard_path(:admin_id => "1") do %>
Manage Users
<% end %>
</li>
<li>
<%= link_to admin_analytics_path(:admin_id => "1") do %>
View Analytics
<% end %>
</li>
</ul>
</li>
<% end %>
<li id="benefit_forms">
<%= link_to user_benefit_forms_path(user_id: current_user.id) do %>
<div class="icon">
<span class="fs1" aria-hidden="true" data-icon="&#xe05c;"></span>
</div>
Benefit Forms
<% end %>
</li>
<li id="retirement">
<%= link_to user_retirement_index_path(user_id: current_user.id) do %>
<div class="icon">
<span class="fs1" aria-hidden="true" data-icon="&#xe096;"></span>
</div>
401k Info
<% end %>
</li>
<li id="pto">
<%= link_to user_paid_time_off_index_path(user_id: current_user.id) do %>
<div class="icon">
<span class="fs1" aria-hidden="true" data-icon="&#xe0d2;"></span>
</div>
PTO
<% end %>
</li>
<li id="employee_info">
<%= link_to user_work_info_index_path(user_id: current_user.id) do %>
<div class="icon">
<span class="fs1" aria-hidden="true" data-icon="&#xe0a9;"></span>
</div>
Work Info
<% end %>
</li>
<li id="performance">
<%= link_to user_performance_index_path(user_id: current_user.id) do %>
<div class="icon">
<span class="fs1" aria-hidden="true" data-icon="&#xe14a;"></span>
</div>
Performance
<% end %>
</li>
<li id="messages">
<%= link_to user_messages_path(user_id: current_user.id) do %>
<div class="icon">
<span class="fs1" aria-hidden="true" data-icon="&#xe040;"></span>
</div>
Messages
<% end %>
</li>
<li id="pay">
<%= link_to user_pay_index_path(user_id: current_user.id) do %>
<div class="icon">
<span class="fs1" aria-hidden="true" data-icon="&#xe038;"></span>
</div>
Pay
<% end %>
</li>
</ul>
</div>
<script type="text/javascript"> <% if is_admin? %>
//Main menu navigation <li class="mt-3">
<div class="px-4 py-2 text-white-50 text-uppercase small fw-bold">Admin</div>
</li>
<li>
<%= link_to admin_dashboard_path(admin_id: "1"), class: "#{controller_name == 'admin' && action_name == 'dashboard' ? 'active' : ''}" do %>
<i class="bi bi-people"></i>
<span>Manage Users</span>
<% end %>
</li>
<li>
<%= link_to admin_analytics_path(admin_id: "1"), class: "#{controller_name == 'admin' && action_name == 'analytics' ? 'active' : ''}" do %>
<i class="bi bi-graph-up"></i>
<span>View Analytics</span>
<% end %>
</li>
<% end %>
$('.submenu > a').click(function(e){ <li class="mt-3">
e.preventDefault(); <div class="px-4 py-2 text-white-50 text-uppercase small fw-bold">Employee</div>
var submenu = $(this).siblings('ul'); </li>
var li = $(this).parents('li');
var submenus = $('#mainnav li.submenu ul');
var submenus_parents = $('#mainnav li.submenu');
if(li.hasClass('open'))
{
if(($(window).width() > 768) || ($(window).width() < 479)) {
submenu.slideUp();
} else {
submenu.fadeOut(250);
}
li.removeClass('open');
} else
{
if(($(window).width() > 768) || ($(window).width() < 479)) {
submenus.slideUp();
submenu.slideDown();
} else {
submenus.fadeOut(250);
submenu.fadeIn(250);
}
submenus_parents.removeClass('open');
li.addClass('open');
}
});
var ul = $('#mainnav > ul'); <li>
<%= link_to user_benefit_forms_path(user_id: current_user.id), class: "#{controller_name == 'benefit_forms' ? 'active' : ''}" do %>
<i class="bi bi-file-earmark-text"></i>
<span>Benefit Forms</span>
<% end %>
</li>
$('#mainnav > a').click(function(e) <li>
{ <%= link_to user_retirement_index_path(user_id: current_user.id), class: "#{controller_name == 'retirement' ? 'active' : ''}" do %>
e.preventDefault(); <i class="bi bi-piggy-bank"></i>
var mainnav = $('#mainnav'); <span>401k Info</span>
if(mainnav.hasClass('open')) <% end %>
{ </li>
mainnav.removeClass('open');
ul.slideUp(250);
} else
{
mainnav.addClass('open');
ul.slideDown(250);
}
});
</script> <li>
<%= link_to user_paid_time_off_index_path(user_id: current_user.id), class: "#{controller_name == 'paid_time_off' ? 'active' : ''}" do %>
<i class="bi bi-calendar-check"></i>
<span>PTO</span>
<% end %>
</li>
<li>
<%= link_to user_work_info_index_path(user_id: current_user.id), class: "#{controller_name == 'work_info' ? 'active' : ''}" do %>
<i class="bi bi-briefcase"></i>
<span>Work Info</span>
<% end %>
</li>
<li>
<%= link_to user_performance_index_path(user_id: current_user.id), class: "#{controller_name == 'performance' ? 'active' : ''}" do %>
<i class="bi bi-bar-chart"></i>
<span>Performance</span>
<% end %>
</li>
<li>
<%= link_to user_messages_path(user_id: current_user.id), class: "#{controller_name == 'messages' ? 'active' : ''}" do %>
<i class="bi bi-envelope"></i>
<span>Messages</span>
<% end %>
</li>
<li>
<%= link_to user_pay_index_path(user_id: current_user.id), class: "#{controller_name == 'pay' ? 'active' : ''}" do %>
<i class="bi bi-credit-card"></i>
<span>Pay</span>
<% end %>
</li>
<li class="mt-4 pt-4 border-top border-secondary">
<div class="px-4 py-2 text-white-50 small">
<i class="bi bi-shield-exclamation"></i>
OWASP RailsGoat <%= Rails::VERSION::STRING %>
</div>
</li>
</ul>
</nav>
<% end %> <% end %>
+289 -118
View File
@@ -1,138 +1,309 @@
<div class="dashboard-wrapper"> <div class="container-fluid">
<div class="main-container"> <!-- Header -->
<!-- Begin Row --> <div class="row mb-4">
<div class="row-fluid"> <div class="col-12">
<!-- Begin Span12 --> <h2 class="mb-3">
<div class="span12"> <i class="bi bi-envelope-fill text-primary"></i> Messages
<div class="widget"> </h2>
<div class="widget-header"> <p class="text-muted">Inbox for <%= current_user.full_name %></p>
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe022;"></span> Messages for <%= current_user.full_name %>
<!--<span class="fs1" aria-hidden="true" data-icon="&#xe006;"><%#= link_to "Send Message", new_user_message_path %></span>-->
</div>
</div>
<div class="widget-body">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width:7%">From:</th>
<th style="width:6%">Date</th>
<th style="width:16%">Message</th>
<th style="width:6%">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<% if @messages.empty? %>
<td><%= "No messages!" %></td><td></td><td></td><td></td>
<% end %>
<% @messages.each do |message| %>
<td><%= message.creator_name %></td>
<td><%= message.created_at.to_date %></td>
<td><%= message.message %></td>
<td><%= link_to "Details", user_message_path(:id => message.id), {:class => "btn btn-info pull-left"}%>
<%= link_to "Delete", user_message_path(:id => message.id), {:method => 'delete', :class => "btn btn-danger pull-left"}%></td>
</tr>
</tbody>
<% end %>
</table>
</div>
</div>
</div>
<!-- End Span12 -->
</div> </div>
<!-- End Row --> </div>
<!-- Begin Row -->
<div class="row-fluid"> <div class="row g-3">
<!-- Begin Span12 --> <!-- Messages Inbox -->
<div class="span12"> <div class="col-lg-8">
<div class="widget"> <div class="card shadow-sm">
<div class="widget-header"> <div class="card-header bg-white py-3">
<div class="title"> <h4 class="mb-0">
<span class="fs1" aria-hidden="true" data-icon="&#xe006;"></span> Send Message <i class="bi bi-inbox text-primary"></i> Inbox
</div> </h4>
<p class="text-muted mb-0 small mt-1">Your received messages</p>
</div>
<div class="card-body p-0">
<% if @messages.any? %>
<div class="messages-list">
<% @messages.each do |message| %>
<div class="message-item">
<div class="message-avatar">
<div class="avatar-circle">
<i class="bi bi-person-fill"></i>
</div>
</div>
<div class="message-content">
<div class="message-header">
<div class="message-from">
<strong><%= message.creator_name %></strong>
</div>
<div class="message-date">
<i class="bi bi-calendar3 me-1"></i>
<%= message.created_at.strftime("%b %d, %Y") %>
</div>
</div>
<div class="message-text">
<%= message.message %>
</div>
<div class="message-actions">
<%= link_to user_message_path(:id => message.id), class: "btn btn-sm btn-outline-primary" do %>
<i class="bi bi-eye"></i> Details
<% end %>
<%= link_to user_message_path(:id => message.id), method: 'delete', data: { confirm: 'Are you sure?' }, class: "btn btn-sm btn-outline-danger" do %>
<i class="bi bi-trash"></i> Delete
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="empty-state">
<i class="bi bi-inbox"></i>
<h5>No Messages Yet</h5>
<p class="text-muted">Your inbox is empty. Send a message to get started!</p>
</div> </div>
<div class="widget-body">
<div id="success" style="display: none;" class="alert alert-block alert-success fade in">
<h4 class="alert-heading">
Success!
</h4>
<p>
Message successfully sent.
</p>
</div>
<div id="failure" style="display: none;" class="alert alert-block alert-error fade in">
<h4 class="alert-heading">
Error!
</h4>
<p>
Failed to send message.
</p>
</div>
<div class="row-fluid">
<div class="span8">
<!-- Begin Message Draft Content-->
<%= form_for @message, :url => user_messages_path, :method => :post, :html => {:id => "send_message"} do |f|%>
<%= f.hidden_field :creator_id, :value => current_user.id %>
<%= f.hidden_field :read, :value => '0' %>
<div class="control-group">
<%= f.label "To:", nil, {:class => "control-label"}%>
<%= f.select(:receiver_id, options_from_collection_for_select(User.all, :id, :full_name)) %>
</div>
<div class="control-group">
<%= f.label :message, nil, {:class => "control-label"}%>
<%= f.text_area :message, {:class => "span12"} %>
</div>
<div class="form-actions no-margin">
<%= f.submit "Submit", {:id => 'submit_button', :class => "btn btn-info pull-right"} %>
</div>
<div class="clearfix"></div>
<% end %> <% end %>
<!-- End Message Draft Content-->
</div>
</div>
</div>
</div>
</div> </div>
<!-- End Span12 --> </div>
</div>
<!-- Send Message Form -->
<div class="col-lg-4">
<div class="card shadow-sm sticky-top" style="top: 80px; border-left: 4px solid var(--rg-success);">
<div class="card-header py-3" style="background: linear-gradient(135deg, rgba(6, 214, 160, 0.05), rgba(30, 130, 94, 0.05));">
<h4 class="mb-0">
<i class="bi bi-send text-success"></i> Send Message
</h4>
<p class="text-muted mb-0 small mt-1">Compose a new message</p>
</div>
<div class="card-body p-4">
<!-- Alert Messages -->
<div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<strong>Success!</strong>
<p class="mb-0 small">Message sent successfully.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<strong>Error!</strong>
<p class="mb-0 small">Failed to send message.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<%= form_for @message, url: user_messages_path, method: :post, html: { id: "send_message" } do |f| %>
<%= f.hidden_field :creator_id, value: current_user.id %>
<%= f.hidden_field :read, value: '0' %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-person-circle text-success me-2"></i>To
</label>
<%= f.select(:receiver_id,
options_from_collection_for_select(User.all, :id, :full_name),
{},
{ class: "form-select form-select-lg" }) %>
<small class="text-muted">Select message recipient</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-chat-left-text text-success me-2"></i>Message
</label>
<%= f.text_area :message,
class: "form-control form-control-lg",
rows: 6,
placeholder: "Type your message here...",
style: "resize: vertical;" %>
<small class="text-muted">Write your message content</small>
</div>
<div class="d-grid">
<%= f.submit "Send Message",
id: 'submit_button',
class: "btn btn-success btn-lg" %>
</div>
<div class="mt-3 p-3 rounded" style="background: var(--rg-light); border-left: 3px solid var(--rg-success);">
<small class="text-muted">
<i class="bi bi-info-circle-fill text-success me-1"></i>
<strong>Tip:</strong> Messages are delivered instantly
</small>
</div>
<% end %>
</div>
</div>
</div> </div>
<!-- End Row -->
</div> </div>
</div> </div>
</body>
</html>
<script type="text/javascript"> <script type="text/javascript">
function makeActive(){
$('li[id="messages"]').addClass('active');
}
$(document).ready(function() {
makeActive();
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
// Form submission with AJAX
$("#submit_button").click(function(event) { $("#submit_button").click(function(event) {
var valuesToSubmit = $("#send_message").serialize(); event.preventDefault();
event.preventDefault(); var valuesToSubmit = $("#send_message").serialize();
$.ajax({
url: <%= "/users/#{current_user.id}/messages.json".inspect.html_safe %>, $.ajax({
url: <%= "/users/#{current_user.id}/messages.json".inspect.html_safe %>,
data: valuesToSubmit, data: valuesToSubmit,
type: "POST", type: "POST",
success: function(response) { success: function(response) {
if (response.msg == "failure") { if (response.msg == "failure") {
$('#failure').show(500).delay(1500).fadeOut(); $('#failure').show(500).delay(2000).fadeOut();
} else { } else {
$('#success').show(500).delay(1500).fadeOut(); $('#success').show(500).delay(2000).fadeOut();
} // Clear form on success
$('#send_message')[0].reset();
// Reload page after delay to show new message
setTimeout(function() {
location.reload();
}, 2500);
}
}, },
error: function(event) { error: function(event) {
$('#failure').show(500).delay(1500).fadeOut(); $('#failure').show(500).delay(2000).fadeOut();
} }
}); });
}); });
function makeActive(){
$('li[id="messages"]').addClass('active');
};
$(document).ready(function () {
makeActive()
});
</script> </script>
<style>
/* Messages List Styling */
.messages-list {
display: flex;
flex-direction: column;
}
.message-item {
display: flex;
gap: 1rem;
padding: 1.5rem;
border-bottom: 1px solid #e9ecef;
transition: background-color 0.2s ease;
}
.message-item:hover {
background-color: rgba(230, 57, 70, 0.03);
}
.message-item:last-child {
border-bottom: none;
}
.message-avatar {
flex-shrink: 0;
}
.avatar-circle {
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, var(--rg-primary) 0%, var(--rg-primary-dark) 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5rem;
box-shadow: 0 2px 8px rgba(230, 57, 70, 0.2);
}
.message-content {
flex: 1;
min-width: 0;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.message-from {
font-size: 1.1rem;
color: var(--rg-dark);
}
.message-date {
font-size: 0.875rem;
color: #6c757d;
white-space: nowrap;
}
.message-text {
color: #495057;
margin-bottom: 1rem;
line-height: 1.6;
word-wrap: break-word;
}
.message-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: #6c757d;
}
.empty-state i {
font-size: 4rem;
opacity: 0.3;
margin-bottom: 1rem;
}
.empty-state h5 {
margin-bottom: 0.5rem;
color: #495057;
}
/* Sticky Form */
@media (min-width: 992px) {
.sticky-top {
position: sticky;
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.message-item {
flex-direction: column;
text-align: center;
}
.message-header {
flex-direction: column;
text-align: center;
}
.message-actions {
justify-content: center;
}
}
</style>
+260 -214
View File
@@ -1,251 +1,297 @@
<div class="dashboard-wrapper"> <div class="container-fluid">
<div class="main-container"> <!-- Alert Messages -->
<div class="row-fluid"> <div class="row">
<div class="span12"> <div class="col-12">
<div id="success" style="display: none;" class="alert alert-block alert-success fade in"> <div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<h4 class="alert-heading"> <div class="d-flex align-items-center">
Success! <i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
</h4> <div>
<p> <h5 class="alert-heading mb-1">Success!</h5>
Information successfully updated. <p class="mb-0">Information successfully updated.</p>
</p> </div>
</div>
</div>
</div>
<div class="row-fluid">
<div class="span12">
<div id="failure" style="display: none;" class="alert alert-block alert-error fade in">
<h4 class="alert-heading">
Error!
</h4>
<p>
Failed to update.
</p>
</div>
</div>
</div>
<!-- Begin DP-->
<div class="row-fluid">
<!-- begin cal -->
<div class="span6">
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe097;"></span> PTO Calendar
</div>
</div>
<div id="calendarDiv" class="widget-body">
<div id='calendar'></div>
</div>
</div>
</div>
<!-- End cal-->
<div class="span6">
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe052;"></span> Schedule PTO
</div>
</div>
<!-- Begin WB-->
<div id="scheduleDiv" class="widget-body">
<%= form_for @schedule, :url => "#",:html => {:id => "cal_update"} do |s|%>
<div class="control-group">
<%= s.label :event_name, "Event Name", {:class => "control-label"}%>
<%= s.text_field :event_name, {:placeholder => "My PTO", :class => "span6"}%>
</div>
<div class="control-group">
<%= s.text_field :event_type, {:type => "hidden", :value => "pto", :class => "span6"}%>
</div>
<div class="control-group">
<%= s.label :event_desc, "Event Description", {:class => "control-label"}%>
<%= s.text_field :event_desc, {:placeholder => "Travel to Europe", :class => "span6"}%>
</div>
<div class="control-group">
<label class="control-label" for="date_range1">
Event Dates
</label>
<div class="controls">
<div class="input-append">
<input type="text" name="date_range1" id="date_range1" class="span8 date_picker" placeholder="Select Date"/>
<span class="add-on">
<i class="icon-calendar"></i>
</span>
</div>
</div>
</div>
<%= s.submit "Submit", {:id => 'cal_update_submit', :class => "btn btn-primary pull-left"} %>
<% end %>
<div class="clearfix">
</div>
</div>
<!-- End WB-->
</div> </div>
</div> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Error!</h5>
<p class="mb-0">Failed to update. Please try again.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div> </div>
<!-- End DP--> </div>
<div class="row-fluid">
<div class="span12"> <!-- Calendar and Schedule Form Row -->
<div class="widget"> <div class="row g-3">
<div class="widget-header"> <!-- PTO Calendar -->
<div class="title"> <div class="col-lg-6">
<span class="fs1" aria-hidden="true" data-icon="&#xe097;"></span> Sick Days <div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-calendar3 text-primary"></i> PTO Calendar
</h4>
</div>
<div id="calendarDiv" class="card-body">
<div id='calendar'></div>
</div>
</div>
</div>
<!-- Schedule PTO Form -->
<div class="col-lg-6">
<div class="card shadow-sm" style="border-left: 4px solid var(--rg-primary);">
<div class="card-header py-3" style="background: linear-gradient(135deg, rgba(230, 57, 70, 0.05), rgba(214, 40, 40, 0.05));">
<h4 class="mb-0">
<i class="bi bi-calendar-plus text-primary"></i> Schedule PTO
</h4>
<p class="text-muted mb-0 small mt-1">Plan your time away from work</p>
</div>
<div id="scheduleDiv" class="card-body p-4">
<%= form_for @schedule, url: "#", html: { id: "cal_update" } do |s| %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-tag-fill text-primary me-2"></i>Event Name
</label>
<%= s.text_field :event_name, {
placeholder: "e.g., Summer Vacation, Personal Day",
class: "form-control form-control-lg"
} %>
</div>
<%= s.text_field :event_type, type: "hidden", value: "pto" %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-chat-left-text-fill text-primary me-2"></i>Event Description
</label>
<%= s.text_field :event_desc, {
placeholder: "e.g., Family trip to Hawaii, Medical appointment",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Optional: Add details about your time off</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold" for="date_range1">
<i class="bi bi-calendar-event-fill text-primary me-2"></i>Event Dates
</label>
<div class="input-group input-group-lg">
<span class="input-group-text bg-white">
<i class="bi bi-calendar-range text-primary"></i>
</span>
<input type="text" name="date_range1" id="date_range1" class="form-control date_picker" placeholder="Click to select date range"/>
</div>
<small class="text-muted">Choose the start and end dates for your PTO</small>
</div>
<div class="d-grid">
<%= s.submit "Schedule PTO", {
id: 'cal_update_submit',
class: "btn btn-primary btn-lg"
} %>
</div>
<div class="mt-3 p-3 rounded" style="background: var(--rg-light); border-left: 3px solid var(--rg-success);">
<small class="text-muted">
<i class="bi bi-info-circle-fill text-primary me-1"></i>
<strong>Tip:</strong> Your PTO request will appear on the calendar after submission
</small>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- Sick Days Stats -->
<div class="row mt-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-bandaid text-primary"></i> Sick Days
</h4>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #579da9;">
<div class="card-body">
<div class="text-muted small mb-1">Days Earned</div>
<h3 class="mb-0" style="color: #579da9;"><%= @pto.sick_days_earned %></h3>
</div> </div>
</div> </div>
<div class="widget-body"> </div>
<div id="column_chart_1"></div> <div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #e26666;">
<div class="card-body">
<div class="text-muted small mb-1">Days Taken</div>
<h3 class="mb-0" style="color: #e26666;"><%= @pto.sick_days_taken %></h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #1e825e;">
<div class="card-body">
<div class="text-muted small mb-1">Days Remaining</div>
<h3 class="mb-0" style="color: #1e825e;"><%= @pto.sick_days_remaining %></h3>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="text-center text-muted mt-3 small">
<i class="bi bi-info-circle"></i> As of today: <%= Date.today.strftime("%B %d, %Y") %>
</div>
</div> </div>
<div class="row-fluid"> </div>
<div class="span12"> </div>
<div class="widget"> </div>
<div class="widget-header">
<div class="title"> <!-- PTO Stats -->
<span class="fs1" aria-hidden="true" data-icon="&#xe097;"></span> Paid Time Off <div class="row mt-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-umbrella-fill text-primary"></i> Paid Time Off
</h4>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #579da9;">
<div class="card-body">
<div class="text-muted small mb-1">Days Earned</div>
<h3 class="mb-0" style="color: #579da9;"><%= @pto.pto_earned %></h3>
</div> </div>
</div> </div>
<div class="widget-body"> </div>
<div id="column_chart_2"></div> <div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #e26666;">
<div class="card-body">
<div class="text-muted small mb-1">Days Taken</div>
<h3 class="mb-0" style="color: #e26666;"><%= @pto.pto_taken %></h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #1e825e;">
<div class="card-body">
<div class="text-muted small mb-1">Days Remaining</div>
<h3 class="mb-0" style="color: #1e825e;"><%= @pto.pto_days_remaining %></h3>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="text-center text-muted mt-3 small">
<i class="bi bi-info-circle"></i> As of today: <%= Date.today.strftime("%B %d, %Y") %>
</div>
</div> </div>
</div>
</div>
</div> </div>
</div> </div>
<%= javascript_include_tag "moment.min.js" %>
<%= javascript_include_tag "fullcalendar.min.js" %>
<script type="text/javascript"> <script type="text/javascript">
function makeActive() {
function makeActive(){
$('li[id="pto"]').addClass('active'); $('li[id="pto"]').addClass('active');
}; }
$(document).ready(function() { $(document).ready(function() {
makeActive();
// Initialize FullCalendar
$('#calendar').fullCalendar({ $('#calendar').fullCalendar({
events: <%= get_pto_schedule_schedule_index_path(:format => "json").inspect.html_safe %>, events: <%= get_pto_schedule_schedule_index_path(:format => "json").inspect.html_safe %>,
height: 'auto',
contentHeight: 'auto',
aspectRatio: 1.5
});
// Initialize date range picker
$('.date_picker').daterangepicker({
opens: 'right',
locale: {
format: 'MM/DD/YYYY'
}
}); });
}); });
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
//Date picker makeActive();
$('.date_picker').daterangepicker({
opens: 'right'
}); });
$(document).ready(function () { // Form submission
drawChart1(),
drawChart2(),
resizeTopWidgets(),
makeActive()
});
google.load("visualization", "1", {
packages: ["corechart"]
});
function drawChart1() {
var data = google.visualization.arrayToDataTable([
['Current Date', 'Days Earned', 'Days Taken', 'Days Remaining'],
[ <%= "As of today: #{Date.today}".inspect.html_safe %>, <%= @pto.sick_days_earned %>, <%= @pto.sick_days_taken %>, <%= @pto.sick_days_remaining %> ], ]);
var options = {
width: 'auto',
height: '160',
backgroundColor: 'transparent',
colors: ['#579da9', '#e26666', '#1e825e'],
tooltip: {
textStyle: {
color: '#666666',
fontSize: 11
},
showColorCode: true
},
legend: {
textStyle: {
color: 'black',
fontSize: 12
}
},
chartArea: {
left: 60,
top: 10,
height: '80%'
},
};
var chart = new google.visualization.ColumnChart(document.getElementById('column_chart_1'));
chart.draw(data, options);
}
function drawChart2() {
var data = google.visualization.arrayToDataTable([
['Current Date', 'Days Earned', 'Days Taken', 'Days Remaining'],
[ <%= "As of today: #{Date.today}".inspect.html_safe %>, <%= @pto.pto_earned %>, <%= @pto.pto_taken %>, <%= @pto.pto_days_remaining %> ], ]);
var options = {
width: 'auto',
height: '160',
backgroundColor: 'transparent',
colors: ['#579da9', '#e26666', '#1e825e'],
tooltip: {
textStyle: {
color: '#666666',
fontSize: 11
},
showColorCode: true
},
legend: {
textStyle: {
color: 'black',
fontSize: 12
}
},
chartArea: {
left: 60,
top: 10,
height: '80%'
},
};
var chart = new google.visualization.ColumnChart(document.getElementById('column_chart_2'));
chart.draw(data, options);
}
function resizeTopWidgets(){
var calHeight = $("#calendarDiv").height();
$("#scheduleDiv").css({'height':calHeight});
};
$("#cal_update_submit").click(function(event) { $("#cal_update_submit").click(function(event) {
var valuesToSubmit = $("#cal_update").serialize(); event.preventDefault();
event.preventDefault(); var valuesToSubmit = $("#cal_update").serialize();
$.ajax({
url: "/schedule.json", $.ajax({
url: "/schedule.json",
data: valuesToSubmit, data: valuesToSubmit,
type: "POST", type: "POST",
success: function(response) { success: function(response) {
if (response.msg == "failure") { if (response.msg == "failure") {
$('#failure').show(500).delay(1500).fadeOut(); $('#failure').show(500).delay(1500).fadeOut();
} else { } else {
$('#success').show(500).delay(1500).fadeOut(); $('#success').show(500).delay(1500).fadeOut();
$('#calendar').fullCalendar('refetchEvents') $('#calendar').fullCalendar('refetchEvents');
} // Clear form
$('#cal_update')[0].reset();
}
}, },
error: function(event) { error: function(event) {
$('#failure').show(500).delay(1500).fadeOut(); $('#failure').show(500).delay(1500).fadeOut();
} }
}); });
}); });
</script> </script>
<style>
/* FullCalendar modern styling */
#calendar {
border-radius: 0.5rem;
}
.fc-toolbar {
background: var(--rg-light);
padding: 1rem;
border-radius: 0.5rem 0.5rem 0 0;
}
.fc-button {
background: var(--rg-primary) !important;
border-color: var(--rg-primary) !important;
border-radius: 0.5rem !important;
text-transform: none !important;
padding: 0.375rem 0.75rem !important;
}
.fc-button:hover {
background: var(--rg-primary-dark) !important;
border-color: var(--rg-primary-dark) !important;
}
.fc-day-header {
background: var(--rg-light);
padding: 0.75rem;
font-weight: 600;
}
.fc-event {
background: var(--rg-primary);
border-color: var(--rg-primary);
border-radius: 0.375rem;
padding: 0.25rem 0.5rem;
}
.fc-today {
background: rgba(230, 57, 70, 0.05) !important;
}
</style>
@@ -1,31 +1,60 @@
<div class="row-fluid"> <div class="rg-login-wrapper">
<div class="span12"> <div class="rg-login-card">
<div class="row-fluid"> <div class="rg-login-header">
<div class="span4 offset4"> <div class="rg-login-logo">
<h2 align="center">MetaCorp</h2> <i class="bi bi-key-fill"></i>
<h3 align="center">A GoatGroup Company</h3> </div>
<h2 class="mb-1">Reset Password</h2>
<p class="text-muted mb-0">We'll send you a reset link</p>
</div>
<div class="signup"> <%= form_tag "forgot_password", html: { class: "needs-validation", novalidate: true } do %>
<%= form_tag "forgot_password", :class=> "signup-wrapper" do %> <div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<div class="header"> <div class="input-group">
<h2>Forgot Password</h2> <span class="input-group-text"><i class="bi bi-envelope"></i></span>
<p>Fill out the form below to reset your password.</p> <%= text_field_tag :email, params[:email], {
</div> class: "form-control",
id: "email",
<div class="content"> placeholder: "you@example.com",
<%= text_field_tag :email, params[:email], {:class => "input input-block-level", :placeholder => "Email"} %> required: true,
</div> autofocus: true,
type: "email"
<div class="actions"> } %>
<%= submit_tag "Reset Password", {:class => "btn btn-info btn-large"} %>
</div>
<div class="clearfix"></div>
<% end %>
</div> </div>
<div class="form-text">Enter the email address associated with your account</div>
</div>
<div class="d-grid gap-2">
<%= submit_tag "Send Reset Link", class: "btn btn-primary btn-lg" %>
</div>
<hr class="my-4">
<div class="text-center">
<p class="text-muted mb-2">Remember your password?</p>
<%= link_to login_path, class: "btn btn-outline-primary" do %>
<i class="bi bi-arrow-left"></i> Back to Login
<% end %>
</div>
<% end %>
<div class="mt-4 p-3 rounded" style="background: linear-gradient(135deg, rgba(69, 123, 157, 0.1), rgba(29, 53, 87, 0.1)); border: 2px solid rgba(69, 123, 157, 0.3);">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle-fill me-2 mt-1" style="font-size: 1.25rem; color: var(--rg-secondary);"></i>
<div class="small">
<strong class="d-block mb-1">Password Reset Help</strong>
If you don't receive an email within a few minutes, check your spam folder or contact support.
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<style>
/* Override main content styling for password reset page */
.rg-main.no-sidebar {
margin: 0;
padding: 0;
}
</style>
@@ -1,39 +1,75 @@
<div class="row-fluid"> <div class="rg-login-wrapper">
<div class="span12"> <div class="rg-login-card">
<div class="row-fluid"> <div class="rg-login-header">
<div class="span4 offset4"> <div class="rg-login-logo">
<h2 align="center">MetaCorp</h2> <i class="bi bi-shield-lock-fill"></i>
<h3 align="center">A GoatGroup Company</h3> </div>
<h2 class="mb-1">Create New Password</h2>
<p class="text-muted mb-0">Choose a strong, unique password</p>
</div>
<!-- TODO --> <!-- TODO: This form is just a placeholder with no working functionality -->
<!-- Create a form that allows a user to reset their password --> <%= form_tag "password_resets", html: { class: "needs-validation", novalidate: true } do %>
<!-- This form is just a placeholder with no working functionality --> <%= hidden_field_tag 'user', Base64.encode64(Marshal.dump(@user)) %>
<div class="signup"> <div class="mb-3">
<%= form_tag "password_resets", :class=> "signup-wrapper" do %> <label for="password" class="form-label">New Password</label>
<div class="input-group">
<div class="header"> <span class="input-group-text"><i class="bi bi-lock"></i></span>
<h2>Create Password</h2> <%= password_field_tag :password, params[:password], {
<p>Fill out the form below to create a new password.</p> class: "form-control",
</div> id: "password",
placeholder: "Enter new password",
<div class="content"> required: true,
<%= hidden_field_tag 'user', Base64.encode64(Marshal.dump(@user)) %> autofocus: true,
<%= label_tag "Enter Password" %> minlength: 6
<%= password_field_tag :password, params[:password], {:class => "input input-block-level"} %> } %>
<%= label_tag "Confirm Password" %>
<%= password_field_tag :confirm_password, params[:confirm_password], {:class => "input input-block-level"} %>
</div>
<div class="actions">
<%= submit_tag "Create Password", {:class => "btn btn-danger btn-large"} %>
</div>
<div class="clearfix"></div>
<% end %>
</div> </div>
<div class="form-text">Password must be at least 6 characters long</div>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Confirm New Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
<%= password_field_tag :confirm_password, params[:confirm_password], {
class: "form-control",
id: "confirm_password",
placeholder: "Re-enter new password",
required: true
} %>
</div>
</div>
<div class="d-grid gap-2 mt-4">
<%= submit_tag "Create New Password", class: "btn btn-primary btn-lg" %>
</div>
<hr class="my-4">
<div class="text-center">
<%= link_to login_path, class: "btn btn-outline-secondary" do %>
<i class="bi bi-arrow-left"></i> Back to Login
<% end %>
</div>
<% end %>
<div class="mt-4 p-3 rounded" style="background: linear-gradient(135deg, rgba(6, 214, 160, 0.1), rgba(17, 138, 178, 0.1)); border: 2px solid rgba(6, 214, 160, 0.3);">
<div class="d-flex align-items-start">
<i class="bi bi-shield-check me-2 mt-1" style="font-size: 1.25rem; color: var(--rg-success);"></i>
<div class="small">
<strong class="d-block mb-1">Password Security Tips</strong>
Use a mix of letters, numbers, and symbols. Avoid common words or personal information.
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<style>
/* Override main content styling for password reset page */
.rg-main.no-sidebar {
margin: 0;
padding: 0;
}
</style>
+306 -168
View File
@@ -1,144 +1,159 @@
<div class="dashboard-wrapper"> <div class="container-fluid">
<div class="main-container"> <!-- Header -->
<div class="row-fluid"> <div class="row mb-4">
<div id="success" style="display: none;" class="alert alert-block alert-success fade in"> <div class="col-12">
<h4 class="alert-heading"> <h2 class="mb-3">
Success! <i class="bi bi-bank text-primary"></i> Direct Deposit & Pay
</h4> </h2>
<p> <p class="text-muted">Manage your direct deposit accounts and payment settings</p>
Information successfully updated.
</p>
</div>
</div> </div>
<div class="row-fluid"> </div>
<div id="failure" style="display: none;" class="alert alert-block alert-error fade in">
<h4 class="alert-heading">
Error!
</h4>
<p>
Failed to update.
</p>
</div>
</div>
<!-- Begin Row-Fluid for Inputs -->
<div class="row-fluid">
<div class="span9">
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe08e;"></span> Direct Deposit
</div>
</div>
<div class="widget-body">
<div class="row-fluid">
<%= form_tag "#", {:class => "form-horizontal", :id => "bank_info_form" } do %>
<!-- Begin inputs-->
<div class="input-append"> <!-- Alert Messages -->
<%= text_field_tag :bank_account_num, params[:bank_account_num], {:placeholder => "Bank Account Number"} %> <div class="row">
<span class="add-on">#</span> <div class="col-12">
</div> <div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<div class="input-append"> <i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
<%= text_field_tag :bank_routing_num, params[:bank_routing_num], {:placeholder => "Bank Routing Number"} %> <div>
<span class="add-on">#</span> <h5 class="alert-heading mb-1">Success!</h5>
</div> <p class="mb-0">Information successfully updated.</p>
<div class="input-append">
<%= text_field_tag :dd_percent, params[:dd_percent], {:placeholder => "Percentage of Deposit"} %>
<span class="add-on">%</span>
</div>
<!-- End Inputs -->
<%= submit_tag "Submit", {:id => "dd_form_btn", :style => "margin-left: 10px;", :class => "btn btn-medium btn-primary"} %>
<% end %>
</div>
</div>
</div>
</div>
</div>
<!-- End Row-Fluid for Inputs-->
<!-- ###################-->
<!-- Begin Dynamic Table ColSpan Table -->
<div class="row-fluid">
<div class="span9">
<div class="widget">
<!-- Begin Widget Header-->
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe14a;"></span> Accounts
</div>
</div>
<!-- End Widget Header-->
<div class="widget-body">
<div id="dt_example" class="example_alt_pagination">
<table class="table table-condensed table-striped table-hover table-bordered pull-left" id="data_table">
<thead>
<tr>
<th style="width:30%">
Encrypted Bank Account Number
<%=link_to "#", { :style => "color:#AA6F93", :id => "encrypted_acct_question"} do %>
<span class="box1">
<span aria-hidden="true" class="icon-question"></span>
</span>
<% end %>
</th>
<th style="width:25%">
Bank Routing Number
</th>
<th style="width:20%">
Percentage of Deposit
</th>
<th style="width:25%">
Action
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div class="clearfix">
</div> </div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
</div> <!-- end of widget body-->
<div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Error!</h5>
<p class="mb-0">Failed to update. Please try again.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
</div> </div>
</div </div>
<!-- End Dynamic Table ColSpan Table -->
<!-- ###################-->
<!-- Begin Row-Fluid for Decryption Input --> <div class="row g-4">
<div class="row-fluid"> <!-- Left Column - Forms -->
<div class="span9"> <div class="col-lg-5">
<div class="widget"> <!-- Add Direct Deposit Form -->
<div class="widget-header"> <div class="card shadow-sm mb-4">
<div class="title"> <div class="card-header bg-white py-3">
<span class="fs1" aria-hidden="true" data-icon="&#xe11b;"></span> Decrypt Bank Account Number <h5 class="mb-0">
</div> <i class="bi bi-plus-circle text-success me-2"></i>Add Direct Deposit
</div> </h5>
<div class="widget-body"> </div>
<div class="row-fluid"> <div class="card-body p-4">
<%= form_tag "#", {:class => "form-horizontal", :id => "decrypt_form" } do %> <%= form_tag "#", { class: "needs-validation", id: "bank_info_form" } do %>
<!-- Begin inputs--> <div class="mb-4">
<label class="form-label fw-semibold">
<div class="input-append"> <i class="bi bi-bank2 text-success me-2"></i>Bank Account Number
<%= text_field_tag :value_to_decrypt, params[:value_to_decrypt], {:placeholder => "Bank Account Number"} %> </label>
<span class="add-on">#</span> <%= text_field_tag :bank_account_num, params[:bank_account_num], {
</div> placeholder: "Enter account number",
class: "form-control form-control-lg"
<!-- End Inputs --> } %>
<%= submit_tag "Submit", {:id => "decrypt_btn", :style => "margin-left: 10px;", :class => "btn btn-medium btn-primary"} %> <small class="text-muted">Your bank account number</small>
<% end %>
</div>
</div>
</div>
</div> </div>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-diagram-3 text-success me-2"></i>Bank Routing Number
</label>
<%= text_field_tag :bank_routing_num, params[:bank_routing_num], {
placeholder: "9-digit routing number",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Usually found at the bottom of checks</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-percent text-success me-2"></i>Percentage of Deposit
</label>
<%= text_field_tag :dd_percent, params[:dd_percent], {
placeholder: "e.g., 100",
class: "form-control form-control-lg"
} %>
<small class="text-muted">What percentage to deposit (1-100)</small>
</div>
<div class="d-grid">
<%= submit_tag "Add Account", {
id: "dd_form_btn",
class: "btn btn-success btn-lg"
} %>
</div>
<% end %>
</div>
</div>
<!-- Decrypt Form -->
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="bi bi-unlock text-warning me-2"></i>Decrypt Account
</h5>
</div>
<div class="card-body p-4">
<%= form_tag "#", { class: "needs-validation", id: "decrypt_form" } do %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-key-fill text-warning me-2"></i>Encrypted Account Number
</label>
<%= text_field_tag :value_to_decrypt, params[:value_to_decrypt], {
placeholder: "Paste encrypted value",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Copy from the table to the right</small>
</div>
<div class="d-grid">
<%= submit_tag "Decrypt", {
id: "decrypt_btn",
class: "btn btn-warning btn-lg"
} %>
</div>
<% end %>
</div>
</div>
</div>
<!-- Right Column - Accounts Table -->
<div class="col-lg-7">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-list-ul text-primary me-2"></i>Your Accounts
</h5>
<button type="button" class="btn btn-sm btn-outline-secondary" id="encrypted_acct_question">
<i class="bi bi-question-circle me-1"></i> Why Encrypted?
</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="data_table">
<thead class="table-light">
<tr>
<th>Account Number</th>
<th>Routing Number</th>
<th>Deposit %</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- DataTable will populate this -->
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
<!-- Row-Fluid for Decryption Input -->
</div> </div>
</div> </div>
@@ -151,9 +166,8 @@
a user to delete that direct deposit entry a user to delete that direct deposit entry
*/ */
function buildDeleteLink(dd_id){ function buildDeleteLink(dd_id){
var link = '<a href="/users/' + '<%= current_user.id %>' + '/pay/'+ dd_id + '" data-method="delete" rel="nofollow" class="delete-row">' + var link = '<a href="/users/' + '<%= current_user.id %>' + '/pay/'+ dd_id + '" data-method="delete" rel="nofollow" class="btn btn-sm btn-outline-danger delete-row">' +
'<i class="icon-trash">'+ '<i class="bi bi-trash"></i> Delete</a>'
'</i></a>'
return link return link
}; };
@@ -163,15 +177,38 @@ function buildDeleteLink(dd_id){
*/ */
function parseDirectDepostInfo(response){ function parseDirectDepostInfo(response){
var msg = jQuery.parseJSON(JSON.stringify(response)); var msg = jQuery.parseJSON(JSON.stringify(response));
var table = $('#data_table').DataTable();
$.each(msg.user, function(index, val){ $.each(msg.user, function(index, val){
$('#data_table').dataTable().fnAddData( [ table.row.add( [
val.bank_account_num, '<code class="text-monospace">' + val.bank_account_num + '</code>',
val.bank_routing_num, '<span class="badge bg-light text-dark">' + val.bank_routing_num + '</span>',
val.percent_of_deposit, '<span class="badge bg-success">' + val.percent_of_deposit + '%</span>',
buildDeleteLink(val.id) buildDeleteLink(val.id)
] ); ] );
}); });
table.draw();
};
/*
createDataTable initializes the dd table as a datatable
*/
function createDataTable(){
// Check if DataTable is already initialized
if ($.fn.DataTable.isDataTable('#data_table')) {
$('#data_table').DataTable().destroy();
}
$('#data_table').DataTable({
"sPaginationType": "full_numbers",
"language": {
"emptyTable": "No direct deposit accounts configured yet"
},
"autoWidth": false,
"searching": true,
"ordering": true
});
}; };
/* /*
@@ -180,7 +217,9 @@ function parseDirectDepostInfo(response){
with the response from the endpoint in order to populate the data table. with the response from the endpoint in order to populate the data table.
*/ */
function populateTable() { function populateTable() {
$('#data_table').dataTable().fnClearTable(); var table = $('#data_table').DataTable();
table.clear();
$.ajax({ $.ajax({
url: <%= sanitize(user_pay_path(:format => "json", user_id: current_user.id, id: current_user.id).inspect) %>, url: <%= sanitize(user_pay_path(:format => "json", user_id: current_user.id, id: current_user.id).inspect) %>,
type: "GET", type: "GET",
@@ -193,25 +232,16 @@ function populateTable() {
}); });
}; };
/*
createDataTable initializes the dd table as a datatable
*/
function createDataTable(){
$('#data_table').dataTable({
"sPaginationType": "full_numbers"
});
};
/* /*
This function doesn't really work right now but is supposed to offer the user a This function doesn't really work right now but is supposed to offer the user a
"delete confirmation" message "delete confirmation" message
*/ */
$('.delete-row').click(function () { $(document).on('click', '.delete-row', function (e) {
var conf = confirm('Continue delete?'); var conf = confirm('Are you sure you want to delete this account?');
if (conf) $(this).parents('tr').fadeOut(function () { if (!conf) {
$(this).remove(); e.preventDefault();
});
return false; return false;
}
}); });
/* /*
@@ -220,7 +250,27 @@ $('.delete-row').click(function () {
*/ */
function decryptShow(response){ function decryptShow(response){
var msg = jQuery.parseJSON(JSON.stringify(response)); var msg = jQuery.parseJSON(JSON.stringify(response));
alert("Decrypted Account Number: " + msg.account_num);
// Modern alert using Bootstrap modal-like appearance
var alertHtml = '<div class="alert alert-info alert-dismissible fade show" role="alert" style="position: fixed; top: 100px; left: 50%; transform: translateX(-50%); z-index: 9999; min-width: 400px; box-shadow: 0 4px 16px rgba(0,0,0,0.2);">' +
'<div class="d-flex align-items-center">' +
'<i class="bi bi-unlock-fill me-3" style="font-size: 2rem;"></i>' +
'<div>' +
'<h5 class="alert-heading mb-1">Decrypted Account Number</h5>' +
'<p class="mb-0"><strong style="font-size: 1.2rem; font-family: monospace;">' + msg.account_num + '</strong></p>' +
'</div>' +
'</div>' +
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' +
'</div>';
$('body').append(alertHtml);
// Auto-remove after 5 seconds
setTimeout(function() {
$('.alert').fadeOut(function() {
$(this).remove();
});
}, 5000);
}; };
/* /*
@@ -239,6 +289,7 @@ $("#decrypt_btn").click(function(event){
success: function(response) { success: function(response) {
$('#success').show(500).delay(1500).fadeOut(); $('#success').show(500).delay(1500).fadeOut();
decryptShow(response); decryptShow(response);
$('#decrypt_form')[0].reset();
}, },
error: function(event) { error: function(event) {
$('#failure').show(500).delay(1500).fadeOut(); $('#failure').show(500).delay(1500).fadeOut();
@@ -260,6 +311,7 @@ $("#dd_form_btn").click(function(event) {
type: "POST", type: "POST",
success: function(response) { success: function(response) {
$('#success').show(500).delay(1500).fadeOut(); $('#success').show(500).delay(1500).fadeOut();
$('#bank_info_form')[0].reset();
populateTable(); populateTable();
}, },
error: function(event) { error: function(event) {
@@ -270,10 +322,27 @@ $("#dd_form_btn").click(function(event) {
$("#encrypted_acct_question").click(function(event) { $("#encrypted_acct_question").click(function(event) {
event.preventDefault(); event.preventDefault();
alert("For your safety your account number is stored encrypted as well as presented to you \nin an encrypted form.\n\n" +
"For your convenience, you can decrypt your bank account number at any time using our\n" + // Create modern Bootstrap modal-like alert
"conveniently located decryption function." var modalHtml = '<div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">' +
) '<div class="modal-dialog modal-dialog-centered">' +
'<div class="modal-content">' +
'<div class="modal-header" style="background: linear-gradient(135deg, rgba(69, 123, 157, 0.1), rgba(29, 53, 87, 0.1)); border-left: 4px solid var(--rg-secondary);">' +
'<h5 class="modal-title"><i class="bi bi-shield-lock-fill text-primary me-2"></i>Why Are Account Numbers Encrypted?</h5>' +
'<button type="button" class="btn-close" onclick="$(this).closest(\'.modal\').remove();"></button>' +
'</div>' +
'<div class="modal-body">' +
'<p class="mb-3"><i class="bi bi-check-circle-fill text-success me-2"></i><strong>For your safety</strong>, your account number is stored encrypted in our database and presented to you in an encrypted form.</p>' +
'<p class="mb-0"><i class="bi bi-unlock-fill text-warning me-2"></i><strong>For your convenience</strong>, you can decrypt your bank account number at any time using our conveniently located decryption function.</p>' +
'</div>' +
'<div class="modal-footer">' +
'<button type="button" class="btn btn-primary" onclick="$(this).closest(\'.modal\').remove();">Got It</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
$('body').append(modalHtml);
}); });
/* /*
@@ -284,14 +353,83 @@ function makeActive(){
}; };
/* /*
1) makeActive - Adds the active class to the sidebar element Initialize page - called on both ready and turbolinks:load
2) createDataTable - Initializes the dataTable as such
3) populateTable - populates the newly initialized dataTable
*/ */
$(document).ready( function initializePage() {
makeActive, makeActive();
createDataTable(), createDataTable();
populateTable() populateTable();
) }
// Handle normal page loads
$(document).ready(function() {
initializePage();
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
initializePage();
});
</script> </script>
<style>
/* DataTables styling adjustments */
.dataTables_wrapper .dataTables_paginate .paginate_button {
padding: 0.375rem 0.75rem;
margin: 0 0.125rem;
border-radius: 0.5rem;
border: 1px solid #dee2e6;
background: white;
color: var(--rg-dark);
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
background: var(--rg-primary);
color: white;
border-color: var(--rg-primary);
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
background: var(--rg-primary);
color: white;
border-color: var(--rg-primary);
}
.dataTables_filter input {
border-radius: 0.75rem;
border: 2px solid #e9ecef;
padding: 0.5rem 1rem;
}
.dataTables_filter input:focus {
border-color: var(--rg-primary);
outline: none;
box-shadow: 0 0 0 3px rgba(230, 57, 70, 0.1);
}
.text-monospace {
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
/* Table hover effect */
#data_table tbody tr {
transition: background-color 0.2s ease;
}
#data_table tbody tr:hover {
background-color: rgba(230, 57, 70, 0.03);
}
/* Override focus colors for specific forms */
#bank_info_form .form-control:focus {
border-color: var(--rg-success);
box-shadow: 0 0 0 3px rgba(6, 214, 160, 0.1);
}
#decrypt_form .form-control:focus {
border-color: var(--rg-warning);
box-shadow: 0 0 0 3px rgba(255, 183, 3, 0.1);
}
</style>
+330 -109
View File
@@ -1,126 +1,347 @@
<div class="dashboard-wrapper"> <div class="container-fluid">
<div class="main-container"> <div class="row mb-4">
<div class="row-fluid"> <div class="col-12">
<div class="span12"> <h2 class="mb-3">
<div class="widget"> <i class="bi bi-graph-up-arrow text-primary"></i> Performance Reviews
<div class="widget-header"> </h2>
<div class="title"> <p class="text-muted">Track your performance history and feedback</p>
<span class="fs1" aria-hidden="true" data-icon="&#xe096;"></span> Performance History Visualization </div>
</div> </div>
</div>
<div class="widget-body"> <!-- Performance Summary Stats -->
<div id="line_chart"></div> <div class="row g-3 mb-4">
</div> <%
</div> total_reviews = @perf.count
avg_score = @perf.any? ? (@perf.sum(&:score).to_f / total_reviews).round(1) : 0
latest_score = @perf.last&.score || 0
highest_score = @perf.any? ? @perf.max_by(&:score).score : 0
%>
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid #579da9;">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-star-fill" style="font-size: 2.5rem; color: #579da9;"></i>
</div> </div>
<div class="row-fluid"> <h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
<div class="span12"> Average Score
<div class="widget"> </h6>
<div class="widget-header"> <h2 class="mb-0" style="color: #579da9; font-weight: 700; font-size: 2.5rem;">
<div class="title"> <%= avg_score %>
<span class="fs1" aria-hidden="true" data-icon="&#xe004;"></span> Performance History </h2>
</div> <p class="text-muted mt-2 mb-0 small">Out of 5.0</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid var(--rg-primary);">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-trophy-fill" style="font-size: 2.5rem; color: var(--rg-primary);"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Highest Score
</h6>
<h2 class="mb-0" style="color: var(--rg-primary); font-weight: 700; font-size: 2.5rem;">
<%= highest_score %>
</h2>
<p class="text-muted mt-2 mb-0 small">Best performance</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid #1e825e;">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-calendar-check-fill" style="font-size: 2.5rem; color: #1e825e;"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Latest Score
</h6>
<h2 class="mb-0" style="color: #1e825e; font-weight: 700; font-size: 2.5rem;">
<%= latest_score %>
</h2>
<p class="text-muted mt-2 mb-0 small">Most recent review</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid #b5799e;">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-file-earmark-text-fill" style="font-size: 2.5rem; color: #b5799e;"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Total Reviews
</h6>
<h2 class="mb-0" style="color: #b5799e; font-weight: 700; font-size: 2.5rem;">
<%= total_reviews %>
</h2>
<p class="text-muted mt-2 mb-0 small">Performance evaluations</p>
</div>
</div>
</div>
</div>
<!-- Performance Timeline -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-graph-up text-primary"></i> Performance Trend
</h4>
<p class="text-muted mb-0 small mt-1">Your performance scores over time</p>
</div>
<div class="card-body p-4">
<% if @perf.any? %>
<div class="performance-timeline">
<% @perf.each_with_index do |p, index| %>
<%
score_percentage = (p.score.to_f / 5.0) * 100
score_color = case p.score
when 5 then '#1e825e'
when 4 then '#579da9'
when 3 then '#ffb703'
else '#e26666'
end
%>
<div class="timeline-item" style="animation-delay: <%= index * 0.1 %>s;">
<div class="timeline-marker">
<div class="timeline-date">
<small class="text-muted"><%= p.date_submitted %></small>
</div>
<div class="timeline-dot" style="background-color: <%= score_color %>;">
<span style="font-weight: 700; color: white; font-size: 0.9rem;"><%= p.score %></span>
</div>
<div class="timeline-reviewer">
<small class="text-muted"><%= p.reviewer_name %></small>
</div>
</div>
<div class="timeline-content">
<div class="progress" style="height: 30px; border-radius: 15px;">
<div class="progress-bar" role="progressbar"
style="width: <%= score_percentage %>%; background-color: <%= score_color %>; font-weight: 600; font-size: 1rem;"
aria-valuenow="<%= p.score %>" aria-valuemin="0" aria-valuemax="5">
<%= p.score %> / 5 - <%= p.comments %>
</div>
</div>
</div>
</div>
<% end %>
</div> </div>
<div class="widget-body"> <% else %>
<table class="table table-bordered table-striped"> <div class="text-center text-muted py-5">
<thead> <i class="bi bi-graph-up" style="font-size: 3rem; opacity: 0.3;"></i>
<p class="mt-3 mb-0">No performance data to display</p>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- Performance History Table -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-table text-primary"></i> Performance History
</h4>
<p class="text-muted mb-0 small mt-1">Detailed review feedback and comments</p>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 20%;">
<i class="bi bi-person-badge me-2"></i>Reviewer
</th>
<th style="width: 15%;">
<i class="bi bi-calendar-event me-2"></i>Date
</th>
<th style="width: 10%;">
<i class="bi bi-star me-2"></i>Score
</th>
<th style="width: 55%;">
<i class="bi bi-chat-left-text me-2"></i>Comments
</th>
</tr>
</thead>
<tbody>
<% if @perf.any? %>
<% @perf.each do |p| %>
<tr>
<td class="fw-semibold">
<div class="d-flex align-items-center">
<div class="rounded-circle bg-primary bg-opacity-10 p-2 me-2">
<i class="bi bi-person-fill text-primary"></i>
</div>
<%= p.reviewer_name %>
</div>
</td>
<td>
<span class="badge bg-light text-dark">
<i class="bi bi-calendar-date me-1"></i><%= p.date_submitted %>
</span>
</td>
<td>
<%
score_color = case p.score
when 5 then 'success'
when 4 then 'primary'
when 3 then 'warning'
else 'danger'
end
%>
<span class="badge bg-<%= score_color %>" style="font-size: 1rem; padding: 0.5rem 0.75rem;">
<%= p.score %> / 5
</span>
</td>
<td>
<div class="text-muted">
<%= p.comments %>
</div>
</td>
</tr>
<% end %>
<% else %>
<tr> <tr>
<th style="width:16%">Reviewer</th> <td colspan="4" class="text-center text-muted py-5">
<th style="width:16%">Date</th> <i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
<th style="width:16%">Score</th> <p class="mt-3 mb-0">No performance reviews available yet</p>
<th style="width:16%">Comments</th> </td>
</tr>
</thead>
<tbody>
<% @perf.each do |p| %>
<tr>
<td><%= p.reviewer_name %></td>
<td><%= p.date_submitted %></td>
<td><%= p.score %></td>
<td><%= p.comments %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
</div>
</div> </div>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
google.load("visualization", "1", {
packages: ["corechart"]
});
function drawChart2() {
var data = google.visualization.arrayToDataTable([
['Year', 'Score'],
<% @perf.each do |p| %>
// Let's just hope this data isn't suspectible during later releases ;-)
[ <%= "#{p.date_submitted}".inspect.html_safe %>, <%= p.score %> ],
<% end %>
]);
var options = {
min: 1,
max: 5,
width: 'auto',
height: '160',
backgroundColor: 'transparent',
colors: ['#e26666', '#579da9', '#1e825e', '#b5799e', '#dba26b'],
tooltip: {
textStyle: {
color: '#666666',
fontSize: 11
},
showColorCode: true
},
legend: {
textStyle: {
color: 'black',
fontSize: 12
}
},
chartArea: {
left: 100,
top: 10
},
focusTarget: 'category',
hAxis: {
textStyle: {
color: 'black',
fontSize: 12
}
},
vAxis: {
textStyle: {
color: 'black',
fontSize: 12
}
},
pointSize: 8,
chartArea: {
left: 60,
top: 10,
height: '80%'
},
lineWidth: 2,
};
var chart = new google.visualization.LineChart(document.getElementById('line_chart'));
chart.draw(data, options);
}
function makeActive(){ function makeActive(){
$('li[id="performance"]').addClass('active'); $('li[id="performance"]').addClass('active');
}; }
$(document).ready(function () { $(document).ready(function() {
drawChart2(), makeActive();
makeActive()
}); });
</script> // Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
</script>
<style>
.hover-stat-card {
transition: all 0.3s ease;
}
.hover-stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15) !important;
}
.hover-stat-card h2 {
transition: transform 0.3s ease;
}
.hover-stat-card:hover h2 {
transform: scale(1.05);
}
.table tbody tr {
transition: background-color 0.2s ease;
}
.table tbody tr:hover {
background-color: rgba(230, 57, 70, 0.03);
}
/* Performance Timeline Styles */
.performance-timeline {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.timeline-item {
display: flex;
align-items: center;
gap: 1.5rem;
opacity: 0;
animation: fadeInUp 0.6s ease forwards;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.timeline-marker {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
gap: 0.5rem;
}
.timeline-dot {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: transform 0.3s ease;
}
.timeline-item:hover .timeline-dot {
transform: scale(1.15);
}
.timeline-content {
flex: 1;
}
.timeline-date, .timeline-reviewer {
text-align: center;
white-space: nowrap;
}
.progress {
transition: transform 0.3s ease;
}
.timeline-item:hover .progress {
transform: translateX(5px);
}
@media (max-width: 768px) {
.timeline-item {
flex-direction: column;
text-align: center;
}
.timeline-marker {
min-width: auto;
}
}
</style>
+171 -74
View File
@@ -1,84 +1,181 @@
<div class="dashboard-wrapper"> <div class="container-fluid">
<div class="main-container"> <div class="row mb-4">
<div class="row-fluid"> <div class="col-12">
<div class="span3"> <!-- Beginning of span--> <h2 class="mb-3">
<div class="widget"> <i class="bi bi-piggy-bank-fill text-primary"></i> 401(k) Retirement Plan
<div class="widget-header"> </h2>
<div class="title"> <p class="text-muted">Your retirement savings summary and employee benefits</p>
<span class="fs1" aria-hidden="true" data-icon="&#xe071;"></span> Employee Contribution
</div>
</div>
<div class="widget-body">
<div class="current-statistics">
<div class="expenses">
<h3><%= @info.employee_contrib %></h3>
</div>
</div>
</div>
</div>
</div> <!-- End of span-->
<div class="span3"> <!-- Beginning of span-->
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe075;"></span> Employer Contribution
</div>
</div>
<div class="widget-body">
<div class="current-statistics">
<div class="signups">
<h3><%= @info.employer_contrib %></h3>
</div>
</div>
</div>
</div>
</div> <!-- End of span-->
<div class="span3"> <!-- Beginning of span-->
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe14a;"></span> Total Contribution
</div>
</div>
<div class="widget-body">
<div class="current-statistics">
<div class="income">
<h3><%= @info.total %></h3>
</div>
</div>
</div>
</div>
</div> <!-- End of span-->
</div> </div>
<div class="row-fluid"> </div>
<div class="span6"> <!-- Beginning of span-->
<div class="widget"> <!-- Contribution Stats -->
<div class="widget-header"> <div class="row g-3 mb-4">
<div class="title"> <!-- Employee Contribution -->
<span class="fs1" aria-hidden="true" data-icon="&#xe0d4;"></span> Employee Services <div class="col-lg-4">
</div> <div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid #579da9;">
</div> <div class="card-body p-4">
<div class="widget-body"> <div class="mb-3">
<p> <i class="bi bi-person-fill-check" style="font-size: 3rem; color: #579da9;"></i>
Saving for retirement can be difficult. Choosing the plan that is right for you is incredibly important. MetaCorp understands this and and offers free one-on-one interaction with a savings counselor. This service is available weekly, Monday thru Wednesday. Sign up through your departments designated finance lead. </div>
</p> <h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
<hr/> Employee Contribution
<p> </h6>
MetaCorp is dedicated to its employees and this service is just one way of showing it! <h2 class="mb-0" style="color: #579da9; font-weight: 700; font-size: 2.5rem;">
</p> <%= @info.employee_contrib %>
</div> </h2>
<p class="text-muted mt-2 mb-0 small">Your contributions to date</p>
</div>
</div>
</div>
<!-- Employer Contribution -->
<div class="col-lg-4">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid #1e825e;">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-building-fill-check" style="font-size: 3rem; color: #1e825e;"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Employer Contribution
</h6>
<h2 class="mb-0" style="color: #1e825e; font-weight: 700; font-size: 2.5rem;">
<%= @info.employer_contrib %>
</h2>
<p class="text-muted mt-2 mb-0 small">MetaCorp matching funds</p>
</div>
</div>
</div>
<!-- Total Contribution -->
<div class="col-lg-4">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid var(--rg-primary); background: linear-gradient(135deg, rgba(230, 57, 70, 0.03), rgba(214, 40, 40, 0.03));">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-cash-stack" style="font-size: 3rem; color: var(--rg-primary);"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Total Contribution
</h6>
<h2 class="mb-0" style="color: var(--rg-primary); font-weight: 700; font-size: 2.5rem;">
<%= @info.total %>
</h2>
<p class="text-muted mt-2 mb-0 small">Combined retirement savings</p>
</div>
</div>
</div>
</div>
<!-- Employee Services Section -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm" style="border-left: 5px solid var(--rg-secondary);">
<div class="card-body p-4">
<div class="row align-items-center">
<div class="col-md-2 text-center mb-3 mb-md-0">
<i class="bi bi-person-workspace" style="font-size: 4rem; color: var(--rg-secondary);"></i>
</div>
<div class="col-md-10">
<h4 class="mb-3">
<i class="bi bi-star-fill text-warning"></i> Employee Retirement Services
</h4>
<p class="mb-3" style="line-height: 1.7;">
Saving for retirement can be difficult. Choosing the plan that is right for you is incredibly important.
MetaCorp understands this and offers <strong>free one-on-one interaction with a savings counselor</strong>.
This service is available weekly, Monday through Wednesday.
</p>
<div class="alert alert-info mb-3" role="alert">
<div class="d-flex align-items-start">
<i class="bi bi-calendar-check me-2" style="font-size: 1.5rem;"></i>
<div>
<strong>How to Sign Up:</strong><br>
Contact your department's designated finance lead to schedule your consultation.
</div> </div>
</div> <!-- End of span--> </div>
</div>
<div class="p-3 rounded" style="background: linear-gradient(135deg, rgba(69, 123, 157, 0.1), rgba(29, 53, 87, 0.1)); border-left: 4px solid var(--rg-secondary);">
<i class="bi bi-heart-fill text-danger"></i>
<strong>MetaCorp is dedicated to its employees</strong> - This service is just one way of showing it!
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Additional Info Cards -->
<div class="row mt-3 g-3">
<div class="col-md-4">
<div class="card shadow-sm h-100" style="border-left: 3px solid #579da9;">
<div class="card-body">
<h6 class="card-title">
<i class="bi bi-graph-up-arrow text-primary"></i> Investment Options
</h6>
<p class="card-text small text-muted">
Choose from a variety of investment funds to match your risk tolerance and retirement goals.
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm h-100" style="border-left: 3px solid #1e825e;">
<div class="card-body">
<h6 class="card-title">
<i class="bi bi-percent text-success"></i> Employer Matching
</h6>
<p class="card-text small text-muted">
MetaCorp matches your contributions up to 6% of your salary to maximize your retirement savings.
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm h-100" style="border-left: 3px solid var(--rg-warning);">
<div class="card-body">
<h6 class="card-title">
<i class="bi bi-shield-check text-warning"></i> Tax Advantages
</h6>
<p class="card-text small text-muted">
Contributions are pre-tax, reducing your taxable income while building your retirement nest egg.
</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
function makeActive() {
$('li[id="retirement"]').addClass('active');
}
function makeActive(){ $(document).ready(makeActive);
$('li[id="retirement"]').addClass('active');
};
$(document).ready(makeActive) // Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
</script>
</script> <style>
.hover-stat-card {
transition: all 0.3s ease;
}
.hover-stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15) !important;
}
.hover-stat-card h2 {
transition: transform 0.3s ease;
}
.hover-stat-card:hover h2 {
transform: scale(1.05);
}
</style>
+95 -48
View File
@@ -1,55 +1,102 @@
<div align="right"> <div class="rg-login-wrapper">
<!-- support for multiple languages coming soon! --> <div class="rg-login-card">
<script> <div class="rg-login-header">
//document.write("<select style=\"width: 100px;\">"); <div class="rg-login-logo">
//document.write("<OPTION value=1>English</OPTION>"); <i class="bi bi-shield-fill-exclamation"></i>
//document.write("<OPTION value=2>Spanish</OPTION>"); </div>
try { <h2 class="mb-1">MetaCorp</h2>
var hashParam = location.hash.split("#")[1]; <p class="text-muted mb-0">A GoatGroup Company</p>
var paramName = hashParam.split('=')[0]; </div>
var paramValue = decodeURIComponent(hashParam.split('=')[1]);
document.write("<OPTION value=3>" + paramValue + "</OPTION>");
} catch(err) {
}
//document.write("</select>");
</script>
</div>
<div class="row-fluid">
<div class="span12">
<div class="row-fluid">
<div class="span4 offset4">
<h2 align="center">MetaCorp</h2>
<h3 align="center">A GoatGroup Company</h3>
<div class="signup"> <%= form_tag "sessions", class: "needs-validation", novalidate: true do %>
<%= form_tag "sessions", :class=> "signup-wrapper" do %> <div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<div class="header"> <div class="input-group">
<h2>Login</h2> <span class="input-group-text"><i class="bi bi-envelope"></i></span>
<p>Fill out the form below to login to your control panel.</p> <%= text_field_tag :email, params[:email], {
</div> class: "form-control",
id: "email",
<div class="content"> placeholder: "you@example.com",
<%= hidden_field_tag :url, @url %> required: true,
<%= text_field_tag :email, params[:email], {:class => "input input-block-level", :placeholder=>"Email"} %> autofocus: true
<%= password_field_tag :password, nil, {:class => "input input-block-level", :placeholder=>"Password"}%> } %>
</div>
<div class="actions">
<%= link_to "Forgot Password", forgot_password_path, {:class=>"pull-left"}%><br/>
<%= submit_tag "Login", {:class => "btn btn-info btn-large pull-right"} %>
<span class="checkbox-wrapper">
<%= check_box_tag :remember_me, 1, params[:remember_me], {:id => "form-terms", :class => "checkbox", :type => "checkbox"} %>
<label class="checkbox-label" for="form-terms"></label> <span class="label-text">Remember</span>
</span>
<div class="clearfix"></div>
<% end %>
</div> </div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
<%= password_field_tag :password, nil, {
class: "form-control",
id: "password",
placeholder: "Enter your password",
required: true
} %>
</div>
</div>
<%= hidden_field_tag :url, @url %>
<div class="mb-3 form-check">
<%= check_box_tag :remember_me, 1, params[:remember_me], {
id: "remember_me",
class: "form-check-input"
} %>
<label class="form-check-label" for="remember_me">
Remember me
</label>
</div>
<div class="d-grid gap-2">
<%= submit_tag "Login", class: "btn btn-primary btn-lg" %>
</div>
<div class="text-center mt-3">
<%= link_to "Forgot Password?", forgot_password_path, class: "text-decoration-none" %>
</div>
<hr class="my-4">
<div class="text-center">
<p class="text-muted mb-2">Don't have an account?</p>
<%= link_to "Sign up now", signup_path, class: "btn btn-outline-primary" %>
</div>
<% end %>
<div class="mt-4 p-3 rounded" style="background: linear-gradient(135deg, rgba(255, 193, 7, 0.1), rgba(255, 152, 0, 0.1)); border: 2px solid rgba(255, 193, 7, 0.3); backdrop-filter: blur(10px);">
<div class="d-flex align-items-start">
<i class="bi bi-exclamation-triangle-fill text-warning me-2 mt-1" style="font-size: 1.25rem;"></i>
<div class="small">
<strong class="d-block mb-1">Security Training Environment</strong>
This is an intentionally vulnerable application for educational purposes.
<a href="https://github.com/OWASP/railsgoat/wiki" target="_blank" class="text-warning fw-semibold text-decoration-none">Learn more →</a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- VULNERABILITY: XSS via URL hash parameter -->
<script>
// support for multiple languages coming soon!
try {
var hashParam = location.hash.split("#")[1];
if (hashParam) {
var paramName = hashParam.split('=')[0];
var paramValue = decodeURIComponent(hashParam.split('=')[1]);
// VULNERABLE: Directly writing user input to DOM
document.write("<div class='alert alert-info mt-3'>" + paramValue + "</div>");
}
} catch(err) {
// Silently fail
}
</script>
<style>
/* Override main content styling for login page */
.rg-main.no-sidebar {
margin: 0;
padding: 0;
}
</style>
+61 -97
View File
@@ -1,115 +1,79 @@
<!-- Begin Modal --> <div class="container mt-4">
<div class="card">
<div class="modal-header"> <div class="card-header bg-warning text-dark d-flex justify-content-between align-items-center">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"> <h4 class="mb-0">
× <i class="bi bi-key"></i> Application Credentials (Spoiler)
</button>
<h4 id="myModalLabel1">
Application Credentials (Spoiler)
</h4> </h4>
<%= link_to root_path, class: "btn btn-sm btn-outline-dark" do %>
<i class="bi bi-x-lg"></i> Close
<% end %>
</div> </div>
<div class="modal-body">
<div class="row"> <div class="card-body">
<div class="span8"> <div class="alert alert-warning" role="alert">
<p>Warning, this is a spoiler</p> <i class="bi bi-exclamation-triangle"></i>
<p>Are you sure you want to see the credentials?</p> <strong>Warning:</strong> This is a spoiler. Are you sure you want to see the credentials?
<div id="creds_hidden" style="display:none"> </div>
<table class="table table-striped table-hover table-bordered pull-left" id="data-table">
<div id="creds_hidden" style="display:none">
<table class="table table-striped table-hover table-bordered">
<thead> <thead>
<tr> <tr>
<th> <th>Email</th>
Email <th>Password</th>
</th> <th>API Key</th>
<th>
Password
</th>
<th>
API Key
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td style="word-wrap:break-word;"> <td style="word-wrap:break-word;">admin@metacorp.com</td>
admin@metacorp.com <td>admin1234</td>
</td> <td>1-01de24d75cffaa66db205278d1cf900bf087a737</td>
<td> </tr>
admin1234 <tr>
</td> <td style="word-wrap:break-word;">jmmastey@metacorp.com</td>
<td> <td>railsgoat!</td>
1-01de24d75cffaa66db205278d1cf900bf087a737 <td>2-050ddd40584978fe9e82840b8b95abb98e4786dc</td>
</td> </tr>
</tr> <tr>
<tr> <td style="word-wrap:break-word;">jim@metacorp.com</td>
<td style="word-wrap:break-word;"> <td>alohaowasp</td>
jmmastey@metacorp.com <td>3-eaa9b4d748d6a8c6a38e24ac1cc2204ebc3541c1</td>
</td> </tr>
<td> <tr>
railsgoat! <td style="word-wrap:break-word;">mike@metacorp.com</td>
</td> <td>motocross1445</td>
<td> <td>4-c809b3d11d272cff8cab1da9e4cdf61137f29d2</td>
2-050ddd40584978fe9e82840b8b95abb98e4786dc </tr>
</td> <tr>
</tr> <td style="word-wrap:break-word;">ken@metacorp.com</td>
<tr> <td>citrusblend</td>
<td style="word-wrap:break-word;"> <td>5-4af604a848ca212cfa3935352aabe9522cf89fdc</td>
jim@metacorp.com </tr>
</td>
<td>
alohaowasp
</td>
<td>
3-eaa9b4d748d6a8c6a38e24ac1cc2204ebc3541c1
</td>
</tr>
<tr>
<td style="word-wrap:break-word;">
mike@metacorp.com
</td>
<td>
motocross1445
</td>
<td>
4-4c809b3d11d272cff8cab1da9e4cdf61137f29d2
</td>
</tr>
<tr>
<td style="word-wrap:break-word;">
ken@metacorp.com
</td>
<td>
citrusblend
</td>
<td>
5-4af604a848ca212cfa3935352aabe9522cf89fdc
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div>
</div>
</div> </div>
<div class="row-fluid">
<div class="text-center mt-3">
<button id="understood" class="btn btn-primary">
<i class="bi bi-eye"></i> I understand - Show Credentials
</button>
</div> </div>
</div> </div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">
Close
</button>
<button id="understood" class="btn btn-primary" aria-hidden="true">
I understand
</button>
<div class="card-footer text-center">
<%= link_to root_path, class: "btn btn-secondary" do %>
<i class="bi bi-arrow-left"></i> Back to Home
<% end %>
</div> </div>
</div>
<!-- End Modal --> </div>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() {
$('#understood').click(function() { $('#understood').click(function() {
$("#creds_hidden").show(); $("#creds_hidden").show();
$(this).hide();
});
}); });
</script> </script>
+111 -28
View File
@@ -1,36 +1,119 @@
<div class="row-fluid"> <div class="rg-login-wrapper">
<div class="span12"> <div class="rg-login-card" style="max-width: 500px;">
<div class="rg-login-header">
<div class="rg-login-logo">
<i class="bi bi-person-plus-fill"></i>
</div>
<h2 class="mb-1">Create Account</h2>
<p class="text-muted mb-0">Join the MetaCorp team</p>
</div>
<div class="row-fluid"> <%= form_for @user, html: { id: "account_edit", class: "needs-validation", novalidate: true } do |f| %>
<div class="span4 offset4"> <div class="mb-3">
<div class="signup"> <label for="email" class="form-label">Email Address</label>
<%= form_for @user, :html => {:id => "account_edit", :class=> "signup-wrapper"} do |f| %> <div class="input-group">
<div class="header"> <span class="input-group-text"><i class="bi bi-envelope"></i></span>
<h2>Sign Up</h2> <%= f.text_field :email, {
<p>Fill out the form below to login</p> class: "form-control",
</div> id: "email",
<div class="content"> placeholder: "you@example.com",
<%= f.text_field :email, {:class => "input input-block-level", :placeholder => "Email"} %> required: true,
<%= f.text_field :first_name, {:class => "input input-block-level", :placeholder => "First Name"} %> autofocus: true
<%= f.text_field :last_name, {:class => "input input-block-level", :placeholder => "Last Name"} %> } %>
<div class="control-group"> </div>
<%= f.password_field :password, {:class => "input input-block-level", :placeholder => "Password (at least six characters)"}%> </div>
</div>
<div class="control-group">
<%= f.password_field :password_confirmation, {:class => "input input-block-level", :placeholder => "Confirm Password"}%>
</div>
</div>
<div class="actions">
<%= f.submit "Submit", {:id => 'submit_button', :class => "btn btn-info btn-large pull-right"} %>
</div>
<div class="clearfix"></div>
<% end %>
</div> <div class="row">
<div class="col-md-6 mb-3">
<label for="first_name" class="form-label">First Name</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<%= f.text_field :first_name, {
class: "form-control",
id: "first_name",
placeholder: "First Name",
required: true
} %>
</div> </div>
</div> </div>
<div class="col-md-6 mb-3">
<label for="last_name" class="form-label">Last Name</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<%= f.text_field :last_name, {
class: "form-control",
id: "last_name",
placeholder: "Last Name",
required: true
} %>
</div>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
<%= f.password_field :password, {
class: "form-control",
id: "password",
placeholder: "At least 6 characters",
required: true,
minlength: 6
} %>
</div>
<div class="form-text">Password must be at least 6 characters long</div>
</div>
<div class="mb-3">
<label for="password_confirmation" class="form-label">Confirm Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
<%= f.password_field :password_confirmation, {
class: "form-control",
id: "password_confirmation",
placeholder: "Re-enter password",
required: true
} %>
</div>
</div>
<div class="d-grid gap-2 mt-4">
<%= f.submit "Create Account", {
id: "submit_button",
class: "btn btn-primary btn-lg"
} %>
</div>
<hr class="my-4">
<div class="text-center">
<p class="text-muted mb-2">Already have an account?</p>
<%= link_to login_path, class: "btn btn-outline-primary" do %>
<i class="bi bi-box-arrow-in-right"></i> Sign in
<% end %>
</div>
<% end %>
<div class="mt-4 p-3 rounded" style="background: linear-gradient(135deg, rgba(6, 214, 160, 0.1), rgba(17, 138, 178, 0.1)); border: 2px solid rgba(6, 214, 160, 0.3);">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle-fill me-2 mt-1" style="font-size: 1.25rem; color: var(--rg-success);"></i>
<div class="small">
<strong class="d-block mb-1">Training Environment</strong>
This application is intentionally vulnerable for security training purposes.
</div>
</div>
</div>
</div> </div>
</div> </div>
<%= javascript_include_tag "validation.js"%> <%= javascript_include_tag "validation.js" %>
<style>
/* Override main content styling for signup page */
.rg-main.no-sidebar {
margin: 0;
padding: 0;
}
</style>
+12
View File
@@ -0,0 +1,12 @@
# frozen_string_literal: true
# Rails 8 ReDoS Protection
# Enable automatic timeout for regular expressions to prevent ReDoS attacks
# Default: 1 second timeout for regex operations
#
# This is a Rails 8 security feature that prevents catastrophic backtracking
# in regular expressions from hanging the application.
#
# See: R8-A1-ReDoS tutorial in wiki for exploitation details
Regexp.timeout = 1.0 # 1 second timeout
+5
View File
@@ -39,6 +39,11 @@ Railsgoat::Application.routes.draw do
resources :tutorials do resources :tutorials do
collection do collection do
get "credentials" get "credentials"
post "redos_email"
post "redos_username"
post "redos_email_safe"
get "supply_chain"
get "check_dependencies"
end end
end end
Binary file not shown.
Binary file not shown.
+6 -9
View File
@@ -1,18 +1,16 @@
# frozen_string_literal: true
# This file is auto-generated from the current state of the database. Instead # This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to # of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition. # incrementally modify your database, and then regenerate this schema definition.
# #
# Note that this schema.rb definition is the authoritative source for your # This file is the source Rails uses to define your schema when running `bin/rails
# database schema. If you need to create the application database on another # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# system, you should be using db:schema:load, not running all the migrations # be faster and is potentially less error prone than running all of your
# from scratch. The latter is a flawed and unsustainable approach (the more migrations # migrations from scratch. Old migrations may fail to apply correctly if those
# you'll amass, the slower it'll run and the greater likelihood for issues). # migrations use external dependencies or application code.
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171007010129) do ActiveRecord::Schema[8.0].define(version: 2017_10_07_010129) do
create_table "analytics", force: :cascade do |t| create_table "analytics", force: :cascade do |t|
t.string "ip_address" t.string "ip_address"
t.string "referrer" t.string "referrer"
@@ -113,5 +111,4 @@ ActiveRecord::Schema.define(version: 20171007010129) do
t.datetime "updated_at" t.datetime "updated_at"
t.binary "encrypted_ssn" t.binary "encrypted_ssn"
end end
end end
+21 -21
View File
@@ -23,21 +23,21 @@ users = [
}, },
{ {
email: "jack@metacorp.com", email: "john@metacorp.com",
admin: false, admin: false,
password: "yankeessuck", password: "yankeessuck",
password_confirmation: "yankeessuck", password_confirmation: "yankeessuck",
first_name: "Jack", first_name: "John",
last_name: "Mannino", last_name: "Smith",
}, },
{ {
email: "jim@metacorp.com", email: "james@metacorp.com",
admin: false, admin: false,
password: "alohaowasp", password: "alohaowasp",
password_confirmation: "alohaowasp", password_confirmation: "alohaowasp",
first_name: "Jim", first_name: "James",
last_name: "Manico", last_name: "Anderson",
}, },
{ {
@@ -70,13 +70,13 @@ users = [
retirements = [ retirements = [
{ {
user: "jack@metacorp.com", user: "john@metacorp.com",
employee_contrib: "1000", employee_contrib: "1000",
employer_contrib: "2000", employer_contrib: "2000",
total: "4500" total: "4500"
}, },
{ {
user: "jim@metacorp.com", user: "james@metacorp.com",
employee_contrib: "8000", employee_contrib: "8000",
employer_contrib: "16000", employer_contrib: "16000",
total: "30000" total: "30000"
@@ -97,14 +97,14 @@ retirements = [
paid_time_off = [ paid_time_off = [
{ {
user: "jack@metacorp.com", user: "john@metacorp.com",
sick_days_taken: 2, sick_days_taken: 2,
sick_days_earned: 5, sick_days_earned: 5,
pto_taken: 5, pto_taken: 5,
pto_earned: 30 pto_earned: 30
}, },
{ {
user: "jim@metacorp.com", user: "james@metacorp.com",
sick_days_taken: 3, sick_days_taken: 3,
sick_days_earned: 6, sick_days_earned: 6,
pto_taken: 3, pto_taken: 3,
@@ -128,7 +128,7 @@ paid_time_off = [
schedule = [ schedule = [
{ {
user: "jack@metacorp.com", user: "john@metacorp.com",
date_begin: Date.new(2014, 7, 30), date_begin: Date.new(2014, 7, 30),
date_end: Date.new(2014, 8, 2), date_end: Date.new(2014, 8, 2),
event_type: "pto", event_type: "pto",
@@ -136,7 +136,7 @@ schedule = [
event_name: "My 2014 Vacation" event_name: "My 2014 Vacation"
}, },
{ {
user: "jim@metacorp.com", user: "james@metacorp.com",
date_begin: Date.new(2013, 9, 1), date_begin: Date.new(2013, 9, 1),
date_end: Date.new(2013, 9, 12), date_end: Date.new(2013, 9, 12),
event_type: "pto", event_type: "pto",
@@ -163,7 +163,7 @@ schedule = [
work_info = [ work_info = [
{ {
user: "jack@metacorp.com", user: "john@metacorp.com",
income: "$50,000", income: "$50,000",
bonuses: "$10,000", bonuses: "$10,000",
years_worked: 2, years_worked: 2,
@@ -171,7 +171,7 @@ work_info = [
DoB: "01-01-1980" DoB: "01-01-1980"
}, },
{ {
user: "jim@metacorp.com", user: "james@metacorp.com",
income: "$40,000", income: "$40,000",
bonuses: "$10,000", bonuses: "$10,000",
years_worked: 1, years_worked: 1,
@@ -198,21 +198,21 @@ work_info = [
performance = [ performance = [
{ {
user: "jack@metacorp.com", user: "john@metacorp.com",
reviewer: 1, reviewer: 1,
comments: "Great job! You are my hero", comments: "Great job! You are my hero",
date_submitted: Date.new(2012, 01, 01), date_submitted: Date.new(2012, 01, 01),
score: 5 score: 5
}, },
{ {
user: "jack@metacorp.com", user: "john@metacorp.com",
reviewer: 1, reviewer: 1,
comments: "Once again, you've done a great job this year. We greatly appreciate your hard work.", comments: "Once again, you've done a great job this year. We greatly appreciate your hard work.",
date_submitted: Date.new(2013, 01, 01), date_submitted: Date.new(2013, 01, 01),
score: 5 score: 5
}, },
{ {
user: "jim@metacorp.com", user: "james@metacorp.com",
reviewer: 1, reviewer: 1,
comments: "Great worker, great attitude for this newcomer!", comments: "Great worker, great attitude for this newcomer!",
date_submitted: Date.new(2013, 01, 01), date_submitted: Date.new(2013, 01, 01),
@@ -251,24 +251,24 @@ performance = [
messages = [ messages = [
{ {
creator: "ken@metacorp.com", creator: "ken@metacorp.com",
receiver: "jack@metacorp.com", receiver: "john@metacorp.com",
message: "Your benefits have been updated.", message: "Your benefits have been updated.",
read: false read: false
}, },
{ {
creator: "mike@metacorp.com", creator: "mike@metacorp.com",
receiver: "jim@metacorp.com", receiver: "james@metacorp.com",
message: "Please update your profile.", message: "Please update your profile.",
read: false read: false
}, },
{ {
creator: "jim@metacorp.com", creator: "james@metacorp.com",
receiver: "mike@metacorp.com", receiver: "mike@metacorp.com",
message: "Welcome to Railsgoat.", message: "Welcome to Railsgoat.",
read: false read: false
}, },
{ {
creator: "jack@metacorp.com", creator: "john@metacorp.com",
receiver: "ken@metacorp.com", receiver: "ken@metacorp.com",
message: "Hello friend.", message: "Hello friend.",
read: false read: false
@@ -0,0 +1,308 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe TutorialsController, type: :controller do
describe "ReDoS vulnerabilities (Rails 8)" do
describe "POST #redos_email" do
context "with valid email" do
it "validates email successfully" do
post :redos_email, params: { email: "test@example.com" }
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response["valid"]).to be true
expect(json_response["message"]).to eq("Email validation completed")
end
it "completes validation in reasonable time" do
post :redos_email, params: { email: "test@example.com" }
json_response = JSON.parse(response.body)
expect(json_response["time_elapsed"]).to be < 0.1 # Should be nearly instant
end
end
context "with potentially malicious ReDoS input" do
it "handles potentially malicious input" do
# Input that could cause catastrophic backtracking in less optimized regex engines
# Note: Ruby 3.3's regex engine is well-optimized and may not timeout with this input
malicious_email = "a" * 30 + "@" + "a" * 30
post :redos_email, params: { email: malicious_email }
# Response may be success (if regex completes) or bad_request (if timeout)
# Both are acceptable outcomes demonstrating the vulnerability
expect(response).to have_http_status(:success).or have_http_status(:bad_request)
json_response = JSON.parse(response.body)
# If it times out, check error message
if response.status == 400
expect(json_response["error"]).to eq("Timeout")
expect(json_response["message"]).to include("ReDoS")
end
end
it "demonstrates the vulnerable pattern exists" do
# This test documents that the pattern is theoretically vulnerable
# even if Ruby 3.3's engine handles it efficiently
malicious_email = "test@example.com"
post :redos_email, params: { email: malicious_email }
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response).to have_key("time_elapsed")
end
end
end
describe "POST #redos_username" do
context "with valid username" do
it "validates username matching pattern" do
post :redos_username, params: { username: "aaaa" }
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response["valid"]).to be true
end
end
context "with potentially malicious ReDoS input" do
it "demonstrates the classic ReDoS pattern (a+)+" do
# This is the classic ReDoS pattern: (a+)+
# Ruby 3.3's engine is optimized but the pattern is still considered vulnerable
malicious_username = "a" * 30 + "!"
post :redos_username, params: { username: malicious_username }
# Ruby 3.3 handles this efficiently, but the pattern is still bad practice
expect(response).to have_http_status(:success).or have_http_status(:bad_request)
json_response = JSON.parse(response.body)
# If it times out, verify the timeout message
if response.status == 400
expect(json_response["error"]).to eq("Timeout")
expect(json_response["time_elapsed"]).to be >= 0.9
end
end
it "demonstrates Rails 8 timeout protection exists" do
malicious_username = "a" * 30 + "!"
# With Rails 8's Regexp.timeout, this won't hang indefinitely
# (In older Ruby versions without timeout, this could hang)
expect {
post :redos_username, params: { username: malicious_username }
}.not_to raise_error # Should not hang, should return response
end
end
end
describe "POST #redos_email_safe" do
context "with valid email" do
it "validates email using safe regex" do
post :redos_email_safe, params: { email: "test@example.com" }
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response["valid"]).to be true
expect(json_response["message"]).to include("safe method")
end
end
context "with potentially malicious input" do
it "handles malicious input safely without timeout" do
malicious_email = "a" * 100 + "@" + "a" * 100 + ".com"
post :redos_email_safe, params: { email: malicious_email }
# Should complete quickly without timeout
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response["time_elapsed"]).to be < 0.1 # Fast even with long input
end
end
end
end
describe "Comparison: Vulnerable vs Safe" do
it "demonstrates the difference between vulnerable and safe patterns" do
# Test vulnerable endpoint with potentially malicious input
post :redos_username, params: { username: "aaaa" }
vulnerable_response = JSON.parse(response.body)
# Test safe endpoint with same type of input
post :redos_email_safe, params: { email: "test@example.com" }
safe_response = JSON.parse(response.body)
# Both should complete (Ruby 3.3 is well-optimized)
expect(vulnerable_response).to have_key("time_elapsed")
expect(safe_response).to have_key("time_elapsed")
# Safe endpoint should use Ruby's built-in URI regex
expect(safe_response["message"]).to include("safe method")
end
it "shows that timeout protection is available" do
# Demonstrates that Regexp.timeout is configured
# This prevents potential hangs even if catastrophic backtracking occurs
expect(Regexp.timeout).to eq(1.0)
end
end
describe "A03:2025 - Software Supply Chain Failures (Rails 8)" do
describe "GET #supply_chain" do
it "returns comprehensive supply chain vulnerability information" do
get :supply_chain
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response).to have_key("vulnerabilities")
expect(json_response).to have_key("demo")
expect(json_response).to have_key("secure_example")
end
it "documents missing SRI vulnerability" do
get :supply_chain
json_response = JSON.parse(response.body)
vulnerabilities = json_response["vulnerabilities"]
sri_vuln = vulnerabilities.find { |v| v["type"] == "Missing Subresource Integrity (SRI)" }
expect(sri_vuln).not_to be_nil
expect(sri_vuln["location"]).to eq("app/views/layouts/application.html.erb")
expect(sri_vuln["description"]).to include("CDN assets loaded without integrity checks")
expect(sri_vuln["impact"]).to include("compromised")
end
it "documents outdated dependencies vulnerability" do
get :supply_chain
json_response = JSON.parse(response.body)
vulnerabilities = json_response["vulnerabilities"]
dep_vuln = vulnerabilities.find { |v| v["type"] == "Outdated Dependencies" }
expect(dep_vuln).not_to be_nil
expect(dep_vuln["mitigation"]).to include("bundle audit")
end
it "documents missing SBOM vulnerability" do
get :supply_chain
json_response = JSON.parse(response.body)
vulnerabilities = json_response["vulnerabilities"]
sbom_vuln = vulnerabilities.find { |v| v["type"] == "No Software Bill of Materials (SBOM)" }
expect(sbom_vuln).not_to be_nil
expect(sbom_vuln["mitigation"]).to include("CycloneDX or SPDX")
end
it "provides secure vs vulnerable examples" do
get :supply_chain
json_response = JSON.parse(response.body)
secure_example = json_response["secure_example"]
expect(secure_example["vulnerable"]).not_to include("integrity=")
expect(secure_example["secure"]).to include("integrity=")
expect(secure_example["secure"]).to include("crossorigin=")
end
it "includes real-world CVE example" do
get :supply_chain
json_response = JSON.parse(response.body)
vulnerabilities = json_response["vulnerabilities"]
sri_vuln = vulnerabilities.find { |v| v["type"] == "Missing Subresource Integrity (SRI)" }
expect(sri_vuln["cve_example"]).to include("British Airways")
expect(sri_vuln["cve_example"]).to include("Magecart")
end
end
describe "GET #check_dependencies" do
it "returns dependency scanning simulation" do
get :check_dependencies
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response["status"]).to eq("scan_complete")
expect(json_response).to have_key("message")
expect(json_response).to have_key("example_vulnerabilities")
expect(json_response).to have_key("recommended_tools")
end
it "provides example vulnerability data" do
get :check_dependencies
json_response = JSON.parse(response.body)
vulnerabilities = json_response["example_vulnerabilities"]
expect(vulnerabilities).to be_an(Array)
expect(vulnerabilities.length).to be >= 2
# Check Rails example
rails_vuln = vulnerabilities.find { |v| v["gem"] == "rails" }
expect(rails_vuln).not_to be_nil
expect(rails_vuln["version"]).to eq("8.0.4")
end
it "recommends security scanning tools" do
get :check_dependencies
json_response = JSON.parse(response.body)
tools = json_response["recommended_tools"]
expect(tools).to be_an(Array)
expect(tools.any? { |t| t.include?("bundle-audit") }).to be true
expect(tools.any? { |t| t.include?("Dependabot") }).to be true
expect(tools.any? { |t| t.include?("Snyk") }).to be true
end
it "includes instructions for manual checking" do
get :check_dependencies
json_response = JSON.parse(response.body)
expect(json_response["note"]).to include("bundle audit")
end
it "handles errors gracefully" do
# Simulate an error by stubbing JSON.parse to raise an error
allow_any_instance_of(TutorialsController).to receive(:render).and_call_original
# The endpoint should handle errors and return 500
# This is more of a structural test to ensure error handling exists
get :check_dependencies
# Should return successful response under normal conditions
expect(response).to have_http_status(:success)
end
end
describe "Integration: Supply Chain Attack Surface" do
it "demonstrates complete attack surface" do
# Check supply chain vulnerabilities
get :supply_chain
supply_response = JSON.parse(response.body)
# Check dependency scanning
get :check_dependencies
dep_response = JSON.parse(response.body)
# Both endpoints should provide complementary information
expect(supply_response["vulnerabilities"].length).to be >= 5
expect(dep_response["recommended_tools"].length).to be >= 4
# Supply chain should reference the tools mentioned in dependency check
expect(supply_response["vulnerabilities"].any? { |v| v["mitigation"]&.include?("bundle audit") }).to be true
end
end
end
end
+2 -2
View File
@@ -9,7 +9,7 @@ SimpleCov.start if ENV["COVERAGE"]
require File.expand_path("../../config/environment", __FILE__) require File.expand_path("../../config/environment", __FILE__)
require "rspec/rails" require "rspec/rails"
require "capybara/rails" require "capybara/rails"
require "capybara/poltergeist" require "selenium-webdriver"
require "database_cleaner" require "database_cleaner"
# Requires supporting ruby files with custom matchers and macros, etc, # Requires supporting ruby files with custom matchers and macros, etc,
@@ -61,6 +61,6 @@ RSpec.configure do |config|
config.infer_spec_type_from_file_location! config.infer_spec_type_from_file_location!
end end
Capybara.javascript_driver = :poltergeist Capybara.javascript_driver = :selenium_headless
DatabaseCleaner.strategy = :truncation DatabaseCleaner.strategy = :truncation