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
source "https://rubygems.org"
#don't upgrade
gem "rails", "6.0.0"
gem "rails", "~> 8.0.0"
ruby "2.6.5"
ruby "3.3.6"
gem "aruba"
gem "bcrypt"
gem "coffee-rails"
gem "execjs"
gem "foreman"
gem "jquery-fileupload-rails"
gem "jquery-rails"
gem "minitest"
gem "powder" # Pow related gem
gem "pry-rails" # not in dev group in case running via prod/staging @ a training
gem "puma"
gem "rails-perftest"
gem "puma", "~> 6.0"
gem "rake"
gem "responders" #For Rails 4.2 # LOCKED DOWN
gem "responders"
gem "ruby-prof"
gem "sassc-rails"
gem "simplecov", require: false, group: :test
gem "sqlite3"
gem "therubyracer"
gem "sqlite3", "~> 2.0"
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
# NOTE: https://github.com/sj26/mailcatcher#bundler
@@ -43,16 +41,15 @@ group :development, :mysql do
gem "pry"
gem "rack-livereload"
gem "rb-fsevent"
gem "rubocop-github"
gem "travis-lint"
gem "rubocop"
end
group :development, :test, :mysql do
gem "capybara"
gem "database_cleaner"
gem "launchy"
gem "poltergeist"
gem "rspec-rails", '4.0.0.beta3' # 4/26/2019: LOCKED DOWN
gem "selenium-webdriver"
gem "rspec-rails"
gem "test-unit"
end
+413 -284
View File
@@ -1,143 +1,181 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (6.0.0)
actionpack (= 6.0.0)
actioncable (8.0.4)
actionpack (= 8.0.4)
activesupport (= 8.0.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.0)
actionpack (= 6.0.0)
activejob (= 6.0.0)
activerecord (= 6.0.0)
activestorage (= 6.0.0)
activesupport (= 6.0.0)
mail (>= 2.7.1)
actionmailer (6.0.0)
actionpack (= 6.0.0)
actionview (= 6.0.0)
activejob (= 6.0.0)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.0)
actionview (= 6.0.0)
activesupport (= 6.0.0)
rack (~> 2.0)
rack-test (>= 0.6.3)
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)
zeitwerk (~> 2.6)
actionmailbox (8.0.4)
actionpack (= 8.0.4)
activejob (= 8.0.4)
activerecord (= 8.0.4)
activestorage (= 8.0.4)
activesupport (= 8.0.4)
mail (>= 2.8.0)
actionmailer (8.0.4)
actionpack (= 8.0.4)
actionview (= 8.0.4)
activejob (= 8.0.4)
activesupport (= 8.0.4)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.4)
actionview (= 8.0.4)
activesupport (= 8.0.4)
nokogiri (>= 1.8.5)
actionview (6.0.0)
activesupport (= 6.0.0)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.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)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.0)
activesupport (= 6.0.0)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.4)
activesupport (= 8.0.4)
globalid (>= 0.3.6)
activemodel (6.0.0)
activesupport (= 6.0.0)
activerecord (6.0.0)
activemodel (= 6.0.0)
activesupport (= 6.0.0)
activestorage (6.0.0)
actionpack (= 6.0.0)
activejob (= 6.0.0)
activerecord (= 6.0.0)
marcel (~> 0.3.1)
activesupport (6.0.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.1, >= 2.1.8)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
aruba (0.14.12)
childprocess (>= 0.6.3, < 4.0.0)
contracts (~> 0.9)
cucumber (>= 1.3.19)
ffi (~> 1.9)
rspec-expectations (>= 2.99)
thor (~> 0.19)
ast (2.4.0)
backports (3.15.0)
bcrypt (3.1.13)
better_errors (2.5.1)
coderay (>= 1.0.0)
activemodel (8.0.4)
activesupport (= 8.0.4)
activerecord (8.0.4)
activemodel (= 8.0.4)
activesupport (= 8.0.4)
timeout (>= 0.4.0)
activestorage (8.0.4)
actionpack (= 8.0.4)
activejob (= 8.0.4)
activerecord (= 8.0.4)
activesupport (= 8.0.4)
marcel (~> 1.0)
activesupport (8.0.4)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
aruba (2.3.2)
bundler (>= 1.17, < 3.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)
rack (>= 0.9.0)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
builder (3.2.3)
bundler-audit (0.6.1)
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
capybara (3.29.0)
rouge (>= 1.0.0)
bigdecimal (3.3.1)
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
builder (3.3.0)
bundler-audit (0.9.3)
bundler (>= 1.2.0)
thor (~> 1.0)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (~> 1.5)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (3.0.0)
cliver (0.3.2)
coderay (1.1.2)
coffee-rails (5.0.0)
coffee-script (>= 2.2.0)
railties (>= 5.2.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.1.5)
contracts (0.16.0)
crass (1.0.5)
cucumber (3.1.2)
builder (>= 2.1.2)
cucumber-core (~> 3.2.0)
cucumber-expressions (~> 6.0.1)
cucumber-wire (~> 0.0.1)
diff-lcs (~> 1.3)
gherkin (~> 5.1.0)
multi_json (>= 1.7.5, < 2.0)
multi_test (>= 0.1.2)
cucumber-core (3.2.1)
backports (>= 3.8.0)
cucumber-tag_expressions (~> 1.1.0)
gherkin (~> 5.0)
cucumber-expressions (6.0.1)
cucumber-tag_expressions (1.1.1)
cucumber-wire (0.0.1)
database_cleaner (1.7.0)
debug_inspector (0.0.3)
diff-lcs (1.3)
docile (1.3.2)
em-websocket (0.5.1)
childprocess (5.1.0)
logger (~> 1.5)
coderay (1.1.3)
concurrent-ruby (1.3.5)
connection_pool (3.0.1)
contracts (0.17.2)
crass (1.0.6)
cucumber (10.1.1)
base64 (~> 0.2)
builder (~> 3.2)
cucumber-ci-environment (> 9, < 11)
cucumber-core (> 15, < 17)
cucumber-cucumber-expressions (> 17, < 19)
cucumber-html-formatter (> 20.3, < 22)
diff-lcs (~> 1.5)
logger (~> 1.6)
mini_mime (~> 1.1)
multi_test (~> 1.1)
sys-uname (~> 1.3)
cucumber-ci-environment (10.0.1)
cucumber-core (15.3.0)
cucumber-gherkin (> 27, < 35)
cucumber-messages (> 26, < 30)
cucumber-tag-expressions (> 5, < 9)
cucumber-cucumber-expressions (18.0.1)
bigdecimal
cucumber-gherkin (34.0.0)
cucumber-messages (> 25, < 29)
cucumber-html-formatter (21.15.1)
cucumber-messages (> 19, < 28)
cucumber-messages (27.2.0)
cucumber-tag-expressions (8.1.0)
database_cleaner (2.1.0)
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)
http_parser.rb (~> 0.6.0)
erubi (1.9.0)
http_parser.rb (~> 0)
erb (6.0.0)
erubi (1.13.1)
eventmachine (1.2.7)
execjs (2.7.0)
ffi (1.11.1)
foreman (0.86.0)
formatador (0.2.5)
gherkin (5.1.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
guard (2.16.1)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
ffi (1.17.2-arm-linux-musl)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
ffi (1.17.2-x86_64-linux-musl)
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)
listen (>= 2.7, < 4.0)
logger (~> 1.6)
lumberjack (>= 1.0.12, < 2.0)
nenv (~> 0.1)
notiffany (~> 0.0)
pry (>= 0.9.12)
ostruct (~> 0.6)
pry (>= 0.13.0)
shellany (~> 0.0)
thor (>= 0.18.1)
guard-compat (1.2.1)
@@ -150,152 +188,216 @@ GEM
guard (~> 2.1)
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
guard-shell (0.7.1)
guard-shell (0.7.2)
guard (>= 2.0.0)
guard-compat (~> 1.0)
http_parser.rb (0.6.0)
i18n (1.7.0)
http_parser.rb (0.8.0)
i18n (1.14.7)
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)
actionpack (>= 3.1)
railties (>= 3.1)
sassc
jquery-rails (4.3.5)
jquery-rails (4.6.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.3.1)
kgio (2.11.2)
launchy (2.4.3)
addressable (~> 2.3)
libv8 (3.16.14.19)
listen (3.2.0)
json (2.17.1)
language_server-protocol (3.17.0.5)
launchy (3.1.1)
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
lint_roller (1.1.0)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.3.1)
logger (1.7.0)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lumberjack (1.0.13)
mail (2.7.1)
nokogiri (>= 1.12.0)
lumberjack (1.4.2)
mail (2.9.0)
logger
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
method_source (0.9.2)
mimemagic (0.3.9)
nokogiri (~> 1)
rake
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.13.0)
multi_json (1.14.1)
multi_test (0.1.2)
mysql2 (0.5.2)
net-imap
net-pop
net-smtp
marcel (1.1.0)
matrix (0.4.3)
memoist3 (1.0.0)
method_source (1.1.0)
mini_mime (1.1.5)
minitest (5.26.2)
multi_json (1.18.0)
multi_test (1.1.0)
mysql2 (0.5.7)
bigdecimal
nenv (0.3.0)
nio4r (2.5.2)
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
net-imap (0.5.12)
date
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)
nenv (~> 0.1)
shellany (~> 0.0)
parallel (1.18.0)
parser (2.6.5.0)
ast (~> 2.4.0)
pg (1.2.3)
poltergeist (1.18.1)
capybara (>= 2.1, < 4)
cliver (~> 0.3.1)
websocket-driver (>= 0.2.0)
powder (0.4.0)
thor (>= 0.11.5)
power_assert (1.1.5)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.1)
puma (4.3.5)
ostruct (0.6.3)
parallel (1.27.0)
parser (3.3.10.0)
ast (~> 2.4.1)
racc
pg (1.6.2)
pg (1.6.2-aarch64-linux)
pg (1.6.2-aarch64-linux-musl)
pg (1.6.2-arm64-darwin)
pg (1.6.2-x86_64-darwin)
pg (1.6.2-x86_64-linux)
pg (1.6.2-x86_64-linux-musl)
power_assert (3.0.1)
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.6.0)
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)
rack (2.2.3)
rack-livereload (0.3.17)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.0)
actioncable (= 6.0.0)
actionmailbox (= 6.0.0)
actionmailer (= 6.0.0)
actionpack (= 6.0.0)
actiontext (= 6.0.0)
actionview (= 6.0.0)
activejob (= 6.0.0)
activemodel (= 6.0.0)
activerecord (= 6.0.0)
activestorage (= 6.0.0)
activesupport (= 6.0.0)
bundler (>= 1.3.0)
railties (= 6.0.0)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
racc (1.8.1)
rack (3.1.19)
rack-livereload (0.6.1)
rack (>= 3.0, < 3.2)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
rails (8.0.4)
actioncable (= 8.0.4)
actionmailbox (= 8.0.4)
actionmailer (= 8.0.4)
actionpack (= 8.0.4)
actiontext (= 8.0.4)
actionview (= 8.0.4)
activejob (= 8.0.4)
activemodel (= 8.0.4)
activerecord (= 8.0.4)
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)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails-perftest (0.0.7)
railties (6.0.0)
actionpack (= 6.0.0)
activesupport (= 6.0.0)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
rainbow (3.0.0)
raindrops (0.19.0)
rake (13.0.0)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
railties (8.0.4)
actionpack (= 8.0.4)
activesupport (= 8.0.4)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
ref (2.0.0)
regexp_parser (1.6.0)
responders (3.0.0)
actionpack (>= 5.0)
railties (>= 5.0)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-core (3.9.0)
rspec-support (~> 3.9.0)
rspec-expectations (3.9.0)
rdoc (6.16.1)
erb
psych (>= 4.0.0)
tsort
regexp_parser (2.11.3)
reline (0.6.3)
io-console (~> 0.5)
responders (3.2.0)
actionpack (>= 7.0)
railties (>= 7.0)
rexml (3.4.4)
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)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.7)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.0.beta3)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.8)
rspec-expectations (~> 3.8)
rspec-mocks (~> 3.8)
rspec-support (~> 3.8)
rspec-support (3.9.0)
rubocop (0.76.0)
jaro_winkler (~> 1.5.1)
rspec-support (~> 3.13.0)
rspec-rails (8.0.2)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.6)
rubocop (1.81.7)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 2.6)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-github (0.13.0)
rubocop (~> 0.70)
rubocop-performance (~> 1.3.0)
rubocop-performance (1.3.0)
rubocop (>= 0.68.0)
ruby-prof (1.0.0)
ruby-progressbar (1.10.1)
sassc (2.2.1)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.48.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
ruby-prof (1.7.2)
base64
ruby-progressbar (1.13.0)
rubyzip (3.2.2)
sassc (2.4.0)
ffi (~> 1.9)
sassc-rails (2.1.2)
railties (>= 4.0.0)
@@ -303,50 +405,81 @@ GEM
sprockets (> 3.0)
sprockets-rails
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)
simplecov (0.17.1)
simplecov (0.22.0)
docile (~> 1.1)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
sprockets (4.0.0)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
sprockets (4.2.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
logger
rack (>= 2.2.4, < 4)
sprockets-rails (3.5.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
sqlite3 (1.4.1)
test-unit (3.3.4)
sqlite3 (2.8.1-aarch64-linux-gnu)
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
therubyracer (0.12.3)
libv8 (~> 3.16.14.15)
ref
thor (0.20.3)
thread_safe (0.3.6)
tilt (2.0.10)
travis-lint (2.0.0)
json
thor (1.4.0)
tilt (2.6.1)
timeout (0.4.4)
tsort (0.2.0)
turbo-rails (2.0.20)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unicode-display_width (1.6.0)
unicorn (5.5.1)
kgio (~> 2.6)
raindrops (~> 0.7)
websocket-driver (0.7.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
uri (1.1.1)
useragent (0.16.11)
websocket (1.2.11)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.2.1)
zeitwerk (2.7.3)
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
aruba
@@ -355,45 +488,41 @@ DEPENDENCIES
binding_of_caller
bundler-audit
capybara
coffee-rails
database_cleaner
execjs
foreman
guard-livereload
guard-rspec
guard-shell
importmap-rails
jquery-fileupload-rails
jquery-rails
launchy
minitest
mysql2
pg
poltergeist
powder
pry
pry-rails
puma
puma (~> 6.0)
rack-livereload
rails (= 6.0.0)
rails-perftest
rails (~> 8.0.0)
rake
rb-fsevent
responders
rspec-rails (= 4.0.0.beta3)
rubocop-github
rspec-rails
rubocop
ruby-prof
sassc-rails
selenium-webdriver
simplecov
sqlite3
sprockets-rails
sqlite3 (~> 2.0)
stimulus-rails
test-unit
therubyracer
travis-lint
turbo-rails
turbolinks
uglifier
unicorn
RUBY VERSION
ruby 2.6.5p114
ruby 3.3.6p108
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
before_action :administrative, if: :admin_param, except: [:get_user]
skip_before_action :has_info
layout false, only: [:get_all_users, :get_user]
layout false, only: [:get_all_users]
def dashboard
end
@@ -38,10 +38,11 @@ class AdminController < ApplicationController
pass = params[:user][:password]
user.password = pass if !(pass.blank?)
user.save!
message = true
end
respond_to do |format|
format.json { render json: { msg: message ? "success" : "failure"} }
flash[:success] = "User updated successfully"
redirect_to admin_get_all_users_path(current_user.id)
else
flash[:error] = "User not found"
redirect_to admin_get_all_users_path(current_user.id)
end
end
@@ -51,11 +52,11 @@ class AdminController < ApplicationController
# Call destroy here so that all association records w/ id are destroyed as well
# Example user.retirement records would be destroyed
user.destroy
message = true
end
respond_to do |format|
format.json { render json: { msg: message ? "success" : "failure"} }
flash[:success] = "User deleted successfully"
else
flash[:error] = "Cannot delete this user"
end
redirect_to admin_get_all_users_path(current_user.id)
end
private
+186 -1
View File
@@ -3,5 +3,190 @@ class TutorialsController < ApplicationController
skip_before_action :has_info
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
+46 -30
View File
@@ -1,35 +1,47 @@
<div class="dashboard-wrapper">
<div class="main-container">
<div class="row-fluid">
<div class="span12">
<div id="success" style="display: none;" class="alert alert-block alert-success fade in">
<h4 class="alert-heading">Success!</h4>
<p>User information successfully updated.</p>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!-- Success Alert -->
<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>
<h5 class="alert-heading mb-1">Success!</h5>
<p class="mb-0">User information successfully updated.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</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>Something went wrong.</p>
<!-- Error Alert -->
<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">Something went wrong. Please try again.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</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>
<!-- 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="widget-body">
</div> <!-- End widget-body-->
</div> <!-- End widget header-->
<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>
@@ -38,17 +50,21 @@
<%= javascript_include_tag "jquery.dataTables.min.js" %>
<script type="text/javascript">
function makeActive() {
$('li[id="admin"]').addClass('active');
};
}
function loadTable() {
$("#userDataTable").load("/admin/"+ <%= params[:admin_id] %> + "/get_all_users")
};
$("#userDataTable").load("/admin/" + <%= params[:admin_id] %> + "/get_all_users");
}
$(document).ready(
makeActive,
loadTable()
);
$(document).ready(function() {
makeActive();
loadTable();
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
</script>
+1 -10
View File
@@ -29,27 +29,18 @@
<%= u.admin ? %{<span class="fs1" aria-label="check" data-icon="&#xe0fe;"}.html_safe : nil %>
</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>
</tr>
<% end %>
</tbody>
</table>
<div id="editAcct" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel1" aria-hidden="true">
</div>
<div class="clearfix">
</div>
</div>
</div>
<script type="text/javascript">
function openEditModal(id){
var link = '/admin/'+ id +'/get_user';
$("#editAcct").load(link);
$("#editAcct").modal('show');
};
function dataTablePagination(){
$('#data-table').dataTable({
"sPaginationType": "full_numbers"
+39 -80
View File
@@ -1,96 +1,55 @@
<!-- Begin Modal -->
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="dismiss">
×
</button>
<h4 id="myModalLabel1">
Account Settings
<div class="container-fluid">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card shadow-sm mt-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
<h4 class="mb-0">
<i class="bi bi-person-gear text-primary"></i> Edit User Account
</h4>
<%= link_to "Back to Users", admin_get_all_users_path(current_user.id), class: "btn btn-outline-secondary btn-sm" %>
</div>
<div class="modal-body">
<div class="row-fluid">
<div class="span8">
<%= form_for @user, :html => {:id => "account_edit"} do |f| %>
<div class="control-group">
<%= f.label :email, nil, {:class => "control-label"}%>
<%= f.text_field :email, {:class => "span12"}%>
<div class="card-body p-4">
<%= form_for @user, url: admin_update_user_path(params[:admin_id]), method: :patch do |f| %>
<div class="mb-3">
<%= f.label :email, nil, {:class => "form-label"}%>
<%= f.text_field :email, {:class => "form-control"}%>
</div>
<div class="control-group">
<%= f.label :first_name, nil, {:class => "control-label"}%>
<%= f.text_field :first_name, {:class => "span12"} %>
<div class="mb-3">
<%= f.label :first_name, nil, {:class => "form-label"}%>
<%= f.text_field :first_name, {:class => "form-control"} %>
</div>
<div class="control-group">
<%= f.label :last_name, nil, {:class => "control-label"}%>
<%= f.text_field :last_name, {:class => "span12"} %>
<div class="mb-3">
<%= f.label :last_name, nil, {:class => "form-label"}%>
<%= f.text_field :last_name, {:class => "form-control"} %>
</div>
<div class="control-group">
<%= f.label :password, nil, {:class => "control-label"}%>
<%= f.password_field :password, {:class => "span12", :placeholder => "Enter Password"}%>
<div class="mb-3">
<%= f.label :password, "Password (leave blank to keep current)", {:class => "form-label"}%>
<%= f.password_field :password, {:class => "form-control", :placeholder => "Enter new password"}%>
</div>
<div class="control-group">
<%= f.label :password_confirmation, nil, {:class => "control-label"}%>
<%= f.password_field :password_confirmation, {:class => "span12", :placeholder => "Enter Password"} %>
<div class="mb-3">
<%= f.label :password_confirmation, nil, {:class => "form-label"}%>
<%= f.password_field :password_confirmation, {:class => "form-control", :placeholder => "Confirm new password"} %>
</div>
<%= f.label :admin, nil, {:class => "control-label"}%>
<%= f.select(:admin, @admin_select) %>
<div class="mb-4">
<%= 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 class="row-fluid">
</div>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">
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>
</div>
</div>
</div>
</div>
</div>
+195 -91
View File
@@ -1,126 +1,230 @@
<div class="dashboard-wrapper">
<div class="main-container">
<div class="row-fluid">
<div class="span4">
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe023;"></span> Health Insurance
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-file-earmark-medical text-primary"></i> Benefit Forms
</h2>
<p class="text-muted">Download benefit documents and upload completed forms</p>
</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>
<!-- Download Forms Section -->
<div class="row g-3 mb-4">
<!-- Health Insurance Card -->
<div class="col-lg-6">
<div class="card shadow-sm h-100 hover-card">
<div class="card-body text-center p-4">
<div class="mb-3">
<i class="bi bi-heart-pulse-fill" style="font-size: 3rem; color: var(--rg-primary);"></i>
</div>
<h4 class="card-title mb-3">Health Insurance</h4>
<p class="text-muted mb-4">Download your health insurance benefit forms and information</p>
<%= link_to download_path(type: "File", name: "public/docs/Health_n_Stuff.pdf"), class: "btn btn-primary btn-lg" do %>
<i class="bi bi-file-earmark-pdf"></i> Download PDF
<% 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>
<!-- Dental Insurance Card -->
<div class="col-lg-6">
<div class="card shadow-sm h-100 hover-card">
<div class="card-body text-center p-4">
<div class="mb-3">
<i class="bi bi-emoji-smile-fill" style="font-size: 3rem; color: var(--rg-success);"></i>
</div>
<h4 class="card-title mb-3">Dental Insurance</h4>
<p class="text-muted mb-4">Download your dental insurance benefit forms and information</p>
<%= link_to download_path(type: "File", name: "public/docs/Dental_n_Stuff.pdf"), class: "btn btn-success btn-lg" do %>
<i class="bi bi-file-earmark-pdf"></i> Download PDF
<% end %>
</div>
<!-- End Widget Body -->
</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="&#xe023;"></span> Health Insurance
<!-- 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>
<!-- Begin Widget Body -->
<div class="widget-body">
<div>
<h2>Upload file</h2>
<%= form_for @benefits, :url => upload_path, :html => { :action => "upload", :multipart => true, :id => "fi" } do |f| %>
<!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload -->
<div>
<div>
<%= hidden_field "benefits", "backup", :value => false %>
<!-- The fileinput-button span is used to style the file input field as button -->
<span class="btn btn-success fileinput-button">
<i class="icon-plus icon-white"></i>
<span>Add file</span>
<%= f.file_field :upload %>
<div class="selected-file mt-3">
<span class="filename text-muted">
<i class="bi bi-file-earmark"></i> No file selected
</span>
<button id="start_upload" type="submit" class="btn btn-primary start">
<i class="icon-upload icon-white"></i>
<span><%= t('fileupload.start_upload') %></span>
</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>
<br/><br/><span class="filename">Nothing selected</span>
</div>
<div class="span5">
<!-- The global progress bar -->
<div class="progress progress-success progress-striped active fade">
<div class="bar" style="width:0%;"></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 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>
<script type="text/javascript">
$(function() {
$("#benefits_upload").change(function (){
var fileName = $(this).val();
$(".filename").html(fileName);
});
});
function makeActive() {
$('li[id="benefit_forms"]').addClass('active');
};
}
$(document).ready(makeActive);
$(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)'
});
}
});
// Form submission handler (for demonstration)
$("#fi").submit(function(e) {
var fileName = $("#benefits_upload").val();
if (!fileName) {
e.preventDefault();
alert('Please select a file to upload');
return false;
}
// Show progress bar
$("#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>
<!-- Google Visualization JS -->
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<div class="p-4">
<div class="table-responsive">
<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">
// google.load("visualization", "1", {packages:["corechart"]});
function drawChart3() {
var data = google.visualization.arrayToDataTable([
['Year', 'Visitors', 'Orders', 'Income', 'Expenses'],
['2007', 300, 800, 900, 300],
['2008', 1170, 860, 1220, 564],
['2009', 260, 1120, 2870, 2340],
['2010', 1030, 540, 3430, 1200],
['2011', 200, 700, 1700, 770],
['2012', 1170, 2160, 3920, 800], ]);
var options = {
width: 'auto',
height: '160',
backgroundColor: 'transparent',
colors: ['#b5799e', '#579da9', '#e26666', '#1e825e', '#dba26b'],
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'));
chart.draw(data, options);
}
$(document).ready(
drawChart3()
);
</script>
<div class="row mt-4 g-3">
<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>
<h3 class="mb-0" style="color: #b5799e;">3,130</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center" style="border-left: 4px solid #579da9;">
<div class="card-body">
<div class="text-muted small mb-1">Total Orders</div>
<h3 class="mb-0" style="color: #579da9;">6,180</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center" style="border-left: 4px solid #e26666;">
<div class="card-body">
<div class="text-muted small mb-1">Total Income</div>
<h3 class="mb-0" style="color: #e26666;">14,040</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center" style="border-left: 4px solid #1e825e;">
<div class="card-body">
<div class="text-muted small mb-1">Total Expenses</div>
<h3 class="mb-0" style="color: #1e825e;">5,174</h3>
</div>
</div>
</div>
</div>
</div>
+67 -33
View File
@@ -1,32 +1,31 @@
<div class="dashboard-wrapper">
<div class="main-container">
<div class="row-fluid">
<div class="span12"> <!--begin span12 -->
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe0a0;"></span> Current Statistics
</div>
<!-- Begin Title Buttons-->
<div class="tools pull-right">
<div class="btn-group">
<button id="change_to_bar_graph" class="btn btn-small">
<span aria-label="change to bar graph" data-icon="&#xe14b;"></span>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
<h4 class="mb-0">
<i class="bi bi-graph-up text-primary"></i> Current Statistics
</h4>
<!-- Chart Type Toggle -->
<div class="btn-group" role="group" aria-label="Chart type selection">
<button id="change_to_bar_graph" class="btn btn-outline-primary btn-sm" title="Bar Graph View" aria-label="Switch to bar graph">
<i class="bi bi-bar-chart-fill"></i> Bar Graph
</button>
<button id="change_to_pie_charts" class="btn btn-small">
<span aria-label="change to pie charts" data-icon="&#xe096;"></span>
<button id="change_to_pie_charts" class="btn btn-outline-primary btn-sm" title="Pie Charts View" aria-label="Switch to pie charts">
<i class="bi bi-pie-chart-fill"></i> Pie Charts
</button>
</div>
</div>
<!-- End Title Buttons-->
<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 id="charts_body" class="widget-body">
<%#= render partial: "dashboard_stats" %>
</div>
<div class="clearfix">
</div>
</div>
</div> <!-- end span12 -->
</div>
</div>
</div>
@@ -34,23 +33,58 @@
<script type="text/javascript">
function makeActive() {
$('li[id="home"]').addClass('active');
};
}
$("#change_to_bar_graph").click(function(event) {
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) {
event.preventDefault();
$("#charts_body").empty()
$("#charts_body").load(<%= sanitize change_graph_dashboard_index_path(:graph => "pie_charts").inspect %>);
})
$(document).ready(
makeActive,
$("#charts_body").load(<%= sanitize change_graph_dashboard_index_path(:graph => "pie_charts").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 pie charts...</p></div>');
// 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>
<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>
+129 -53
View File
@@ -48,8 +48,18 @@
<% end %>
<script type="text/javascript">
// Store timeout IDs so we can clear them on page navigation
var chartTimeouts = [];
function pieChartHome() {
// Clear any existing timeouts first
chartTimeouts.forEach(function(id) { clearTimeout(id); });
chartTimeouts = [];
$(function () {
// Only initialize if chart elements exist
if ($('.chart1').length === 0) return;
//create instance
$('.chart1').easyPieChart({
animate: 2000,
@@ -60,24 +70,37 @@ function pieChartHome() {
lineWidth: 7,
});
//update instance after 5 sec
setTimeout(function () {
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(50);
}, 5000);
setTimeout(function () {
}
}, 5000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(70);
}, 10000);
setTimeout(function () {
}
}, 10000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(30);
}, 15000);
setTimeout(function () {
}
}, 15000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(90);
}, 19000);
setTimeout(function () {
}
}, 19000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(40);
}, 32000);
}
}, 32000));
});
$(function () {
// Only initialize if chart elements exist
if ($('.chart2').length === 0) return;
//create instance
$('.chart2').easyPieChart({
animate: 2000,
@@ -88,24 +111,37 @@ function pieChartHome() {
lineWidth: 7,
});
//update instance after 5 sec
setTimeout(function () {
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(90);
}, 10000);
setTimeout(function () {
}
}, 10000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(40);
}, 18000);
setTimeout(function () {
}
}, 18000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(70);
}, 28000);
setTimeout(function () {
}
}, 28000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(50);
}, 32000);
setTimeout(function () {
}
}, 32000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(80);
}, 40000);
}
}, 40000));
});
$(function () {
// Only initialize if chart elements exist
if ($('.chart3').length === 0) return;
//create instance
$('.chart3').easyPieChart({
animate: 2000,
@@ -116,24 +152,37 @@ function pieChartHome() {
lineWidth: 7,
});
//update instance after 5 sec
setTimeout(function () {
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(20);
}, 9000);
setTimeout(function () {
}
}, 9000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(59);
}, 20000);
setTimeout(function () {
}
}, 20000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(38);
}, 35000);
setTimeout(function () {
}
}, 35000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(79);
}, 49000);
setTimeout(function () {
}
}, 49000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(96);
}, 52000);
}
}, 52000));
});
$(function () {
// Only initialize if chart elements exist
if ($('.chart4').length === 0) return;
//create instance
$('.chart4').easyPieChart({
animate: 2000,
@@ -144,24 +193,37 @@ function pieChartHome() {
lineWidth: 7,
});
//update instance after 5 sec
setTimeout(function () {
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(40);
}, 6000);
setTimeout(function () {
}
}, 6000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(67);
}, 14000);
setTimeout(function () {
}
}, 14000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(43);
}, 23000);
setTimeout(function () {
}
}, 23000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(80);
}, 36000);
setTimeout(function () {
}
}, 36000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(66);
}, 41000);
}
}, 41000));
});
$(function () {
// Only initialize if chart elements exist
if ($('.chart5').length === 0) return;
//create instance
$('.chart5').easyPieChart({
animate: 3000,
@@ -172,28 +234,42 @@ function pieChartHome() {
lineWidth: 7,
});
//update instance after 5 sec
setTimeout(function () {
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(30);
}, 9000);
setTimeout(function () {
}
}, 9000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(87);
}, 19000);
setTimeout(function () {
}
}, 19000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(28);
}, 27000);
setTimeout(function () {
}
}, 27000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(69);
}, 39000);
setTimeout(function () {
}
}, 39000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(99);
}, 47000);
}
}, 47000));
});
}
$(document).ready(
pieChartHome()
);
$(document).ready(pieChartHome);
// Clear timeouts when navigating away with Turbolinks
$(document).on('turbolinks:before-visit', function() {
chartTimeouts.forEach(function(id) { clearTimeout(id); });
chartTimeouts = [];
});
</script>
+419 -24
View File
@@ -1,43 +1,438 @@
<!DOCTYPE html>
<html>
<html lang="en" data-bs-theme="light">
<head>
<title>RailsGoat</title>
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RailsGoat - OWASP Security Training</title>
<%#= 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]
%>
<style>body { font-size:<%= raw cookies[:font] %> !important;}</style>
body { font-size:<%= raw cookies[:font] %> !important;}
<%
end
%>
</style>
</head>
<body>
<%= render "layouts/shared/header" %>
<%= render "layouts/shared/sidebar" %>
<div class="container-fluid">
<% if current_user %>
<div class="dashboard-wrapper">
<main class="rg-main <%= 'no-sidebar' unless current_user %>">
<%= render "layouts/shared/messages" %>
<%= yield %>
</div>
<% else %>
<div class="login-wrapper">
<%= render "layouts/shared/messages" %>
<%= yield %>
</div>
<% end %>
</div>
</main>
<%= render "layouts/shared/footer" %>
<script type="text/javascript">
//Dropdown
$('.dropdown-toggle').dropdown();
</script>
</body>
</html>
+49 -15
View File
@@ -1,22 +1,56 @@
<footer>
<p align="center">
&copy; The Open Worldwide Application Security Project - OWASP, 2015
<% if current_user %>
<footer class="border-top mt-5 py-4 text-center text-muted bg-white">
<div class="container">
<div class="row">
<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
$(function () {
$.scrollUp({
scrollName: 'scrollUp', // Element ID
topDistance: '300', // Distance from top before showing element (px)
topSpeed: 300, // Speed back to top (ms)
animation: 'fade', // Fade, slide, none
animationInSpeed: 400, // Animation in speed (ms)
animationOutSpeed: 400, // Animation out speed (ms)
scrollText: 'Scroll to top', // Text for element
activeOverlay: false, // Set CSS color to display scrollUp active point, e.g '#00FFFF'
<script>
// Modern scroll-to-top without jQuery
(function() {
const scrollBtn = document.getElementById('scrollTopBtn');
if (scrollBtn) {
// Show/hide button based on scroll position
window.addEventListener('scroll', function() {
if (window.pageYOffset > 300) {
scrollBtn.style.display = 'block';
} else {
scrollBtn.style.display = 'none';
}
});
// Scroll to top on click
scrollBtn.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
})();
</script>
+96 -69
View File
@@ -1,88 +1,115 @@
<% if current_user %>
<header>
<span style="color:#eee;margin-left:10px;">
Font Size:
<a data-no-turbolink='true' href="/dashboard/home?font=8pt" style="font-size:10pt;color:#eee" aria-label="small font">A</a>
<a data-no-turbolink='true' href="/dashboard/home?font=200%25" style="font-size:18pt;color:#eee;" aria-label="large font">A</a>
</span>
<div class="user-profile">
<button data-toggle="dropdown" class="dropdown-toggle">
<img src=" <%= image_path('profile_color.jpg')%>" alt="profile">
</button>
<span class="caret"></span>
<ul class="dropdown-menu pull-right">
<li>
<%= link_to "Account settings", user_account_settings_path(user_id: current_user.id) %>
</li>
<li>
<%= link_to "Logout", logout_path %>
</li>
</ul>
<!-- Authenticated Header -->
<header class="rg-header">
<div class="container-fluid h-100">
<div class="row h-100 align-items-center">
<div class="col-auto">
<a href="<%= home_dashboard_index_path %>" class="rg-brand">
<i class="bi bi-shield-fill-exclamation"></i> RailsGoat
</a>
</div>
<div class="col"></div>
<div class="col-auto">
<div class="d-flex align-items-center gap-3">
<!-- Font Size Controls -->
<div class="btn-group btn-group-sm" role="group" aria-label="Font size controls">
<a href="/dashboard/home?font=8pt" class="btn btn-outline-secondary" style="font-size: 10pt;" title="Small font" aria-label="Small font">
<i class="bi bi-type"></i>
</a>
<a href="/dashboard/home?font=200%25" class="btn btn-outline-secondary" style="font-size: 14pt;" title="Large font" aria-label="Large font">
<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>
<ul class="mini-nav">
<li style="color: #FFFFFF">
<!--
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
-->
Welcome, <%= current_user.first_name.html_safe %>
</li>
</ul>
<ul class="mini-nav">
<span class="text-dark"><%= current_user.first_name.html_safe %></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<%= button_to "Visit Tutorial", nil,
{
:class => "btn",
:method => "get",
:onclick => "window.open('https://github.com/OWASP/railsgoat/wiki', '_blank')"
} %>
<%= 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>
</ul>
</header>
<% else %>
<!-- Want to use this template whether auth'd or not so I've got some code to determine how to render below -->
<header>
<ul class="mini-nav">
<li><hr class="dropdown-divider"></li>
<li>
<%= button_to "Signup", signup_path, {:class => "btn btn-primary", :method => "get"} %>
</li>
</ul>
<ul class="mini-nav">
<li>
<%= button_to "login", login_path, {:class => "btn", :method => "get"} %>
</li>
</ul>
<ul class="mini-nav">
<li>
<%= button_to "Tutorial Credentials", "#myModalLabel1", {:id => "show_creds_btn", :class => "btn btn-danger", :method => "get"} %>
</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')"
} %>
<%= 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>
</header>
<div id="modal_div" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myAlert" aria-hidden="true">
<% else %>
<!-- Unauthenticated Header -->
<header class="rg-header">
<div class="container-fluid h-100">
<div class="row h-100 align-items-center">
<div class="col-auto">
<a href="<%= login_path %>" class="rg-brand">
<i class="bi bi-shield-fill-exclamation"></i> RailsGoat
</a>
</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>
<div class="col"></div>
<div class="col-auto">
<div class="d-flex align-items-center gap-2">
<%= link_to credentials_tutorials_path, class: "btn btn-sm btn-warning" do %>
<i class="bi bi-key"></i> Demo Credentials
<% end %>
<%= 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 %>
<%= 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>
<% end %>
+33 -14
View File
@@ -1,19 +1,38 @@
<% flash.each do |name, msg| %>
<% name = name.to_sym %>
<% if name == :error %>
<div class="alert alert-error">
<a class="close" aria-label="dismiss" data-dismiss="alert" href="#">×</a>
<%= content_tag :div, msg, :id => "flash_notice" %>
<%
alert_class = case name
when :error, :alert
'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>
<% elsif name == :success %>
<div class="alert alert-success">
<a class="close" aria-label="dismiss" data-dismiss="alert" href="#">×</a>
<%= 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" %>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<% end %>
<% end %>
+78 -130
View File
@@ -1,142 +1,90 @@
<% if current_user %>
<div id="mainnav" class="hidden-phone hidden-tablet">
<ul style="display: block;">
<li id="home">
<%= link_to home_dashboard_index_path do %>
<div class="icon">
<span class="fs1" aria-hidden="true" data-icon="&#xe0a0;"></span>
</div>
Dashboard
<nav class="rg-sidebar">
<ul class="rg-sidebar-nav">
<li>
<%= link_to home_dashboard_index_path, class: "#{controller_name == 'dashboard' ? 'active' : ''}" do %>
<i class="bi bi-speedometer2"></i>
<span>Dashboard</span>
<% 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 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") do %>
Manage Users
<%= 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") do %>
View Analytics
<%= 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 %>
<li class="mt-3">
<div class="px-4 py-2 text-white-50 text-uppercase small fw-bold">Employee</div>
</li>
<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>
<li>
<%= link_to user_retirement_index_path(user_id: current_user.id), class: "#{controller_name == 'retirement' ? 'active' : ''}" do %>
<i class="bi bi-piggy-bank"></i>
<span>401k Info</span>
<% end %>
</li>
<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>
</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">
//Main menu navigation
$('.submenu > a').click(function(e){
e.preventDefault();
var submenu = $(this).siblings('ul');
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');
$('#mainnav > a').click(function(e)
{
e.preventDefault();
var mainnav = $('#mainnav');
if(mainnav.hasClass('open'))
{
mainnav.removeClass('open');
ul.slideUp(250);
} else
{
mainnav.addClass('open');
ul.slideDown(250);
}
});
</script>
</nav>
<% end %>
+280 -109
View File
@@ -1,138 +1,309 @@
<div class="dashboard-wrapper">
<div class="main-container">
<!-- Begin Row -->
<div class="row-fluid">
<!-- Begin Span12 -->
<div class="span12">
<div class="widget">
<div class="widget-header">
<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 class="container-fluid">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-envelope-fill text-primary"></i> Messages
</h2>
<p class="text-muted">Inbox for <%= current_user.full_name %></p>
</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 %>
<div class="row g-3">
<!-- Messages Inbox -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-inbox text-primary"></i> Inbox
</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| %>
<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>
<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 %>
</table>
</div>
</div>
</div>
<!-- End Span12 -->
</div>
<!-- End Row -->
<!-- Begin Row -->
<div class="row-fluid">
<!-- Begin Span12 -->
<div class="span12">
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe006;"></span> Send Message
</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>
<%= 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>
<% end %>
</div>
</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 %>
<!-- End Message Draft Content-->
</div>
</div>
</div>
</div>
</div>
<!-- End Span12 -->
</div>
<!-- End Row -->
</div>
</div>
</body>
</html>
<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) {
var valuesToSubmit = $("#send_message").serialize();
event.preventDefault();
var valuesToSubmit = $("#send_message").serialize();
$.ajax({
url: <%= "/users/#{current_user.id}/messages.json".inspect.html_safe %>,
data: valuesToSubmit,
type: "POST",
success: function(response) {
if (response.msg == "failure") {
$('#failure').show(500).delay(1500).fadeOut();
$('#failure').show(500).delay(2000).fadeOut();
} 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) {
$('#failure').show(500).delay(1500).fadeOut();
$('#failure').show(500).delay(2000).fadeOut();
}
});
});
function makeActive(){
$('li[id="messages"]').addClass('active');
};
$(document).ready(function () {
makeActive()
});
</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>
+232 -186
View File
@@ -1,235 +1,237 @@
<div class="dashboard-wrapper">
<div class="main-container">
<div class="row-fluid">
<div class="span12">
<div id="success" style="display: none;" class="alert alert-block alert-success fade in">
<h4 class="alert-heading">
Success!
<div class="container-fluid">
<!-- Alert Messages -->
<div class="row">
<div class="col-12">
<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>
<h5 class="alert-heading mb-1">Success!</h5>
<p class="mb-0">Information successfully updated.</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>
<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>
<!-- Calendar and Schedule Form Row -->
<div class="row g-3">
<!-- PTO Calendar -->
<div class="col-lg-6">
<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>
<p>
Information successfully updated.
</p>
</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="calendarDiv" class="card-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
<!-- 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>
<!-- 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
<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>
<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>
<%= 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>
<%= s.submit "Submit", {:id => 'cal_update_submit', :class => "btn btn-primary pull-left"} %>
<% end %>
<div class="clearfix">
</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>
<!-- End WB-->
<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>
<!-- End DP-->
<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="&#xe097;"></span> Sick Days
<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 class="widget-body">
<div id="column_chart_1"></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 class="row-fluid">
<div class="span12">
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe097;"></span> Paid Time Off
</div>
</div>
<div class="widget-body">
<div id="column_chart_2"></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>
<%= javascript_include_tag "moment.min.js" %>
<%= javascript_include_tag "fullcalendar.min.js" %>
<!-- PTO 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-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 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 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>
<script type="text/javascript">
function makeActive() {
$('li[id="pto"]').addClass('active');
};
}
$(document).ready(function() {
makeActive();
// Initialize FullCalendar
$('#calendar').fullCalendar({
events: <%= get_pto_schedule_schedule_index_path(:format => "json").inspect.html_safe %>,
});
height: 'auto',
contentHeight: 'auto',
aspectRatio: 1.5
});
//Date picker
// Initialize date range picker
$('.date_picker').daterangepicker({
opens: 'right'
opens: 'right',
locale: {
format: 'MM/DD/YYYY'
}
});
});
$(document).ready(function () {
drawChart1(),
drawChart2(),
resizeTopWidgets(),
makeActive()
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
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});
};
// Form submission
$("#cal_update_submit").click(function(event) {
var valuesToSubmit = $("#cal_update").serialize();
event.preventDefault();
var valuesToSubmit = $("#cal_update").serialize();
$.ajax({
url: "/schedule.json",
data: valuesToSubmit,
@@ -239,7 +241,9 @@ $("#cal_update_submit").click(function(event) {
$('#failure').show(500).delay(1500).fadeOut();
} else {
$('#success').show(500).delay(1500).fadeOut();
$('#calendar').fullCalendar('refetchEvents')
$('#calendar').fullCalendar('refetchEvents');
// Clear form
$('#cal_update')[0].reset();
}
},
error: function(event) {
@@ -247,5 +251,47 @@ $("#cal_update_submit").click(function(event) {
}
});
});
</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="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 "forgot_password", :class=> "signup-wrapper" do %>
<div class="header">
<h2>Forgot Password</h2>
<p>Fill out the form below to reset your password.</p>
<div class="rg-login-wrapper">
<div class="rg-login-card">
<div class="rg-login-header">
<div class="rg-login-logo">
<i class="bi bi-key-fill"></i>
</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="content">
<%= text_field_tag :email, params[:email], {:class => "input input-block-level", :placeholder => "Email"} %>
<%= form_tag "forgot_password", html: { class: "needs-validation", novalidate: true } do %>
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
<%= text_field_tag :email, params[:email], {
class: "form-control",
id: "email",
placeholder: "you@example.com",
required: true,
autofocus: true,
type: "email"
} %>
</div>
<div class="form-text">Enter the email address associated with your account</div>
</div>
<div class="actions">
<%= submit_tag "Reset Password", {:class => "btn btn-info btn-large"} %>
<div class="d-grid gap-2">
<%= submit_tag "Send Reset Link", class: "btn btn-primary btn-lg" %>
</div>
<div class="clearfix"></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>
<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="span12">
<div class="row-fluid">
<div class="span4 offset4">
<h2 align="center">MetaCorp</h2>
<h3 align="center">A GoatGroup Company</h3>
<!-- TODO -->
<!-- Create a form that allows a user to reset their password -->
<!-- This form is just a placeholder with no working functionality -->
<div class="signup">
<%= form_tag "password_resets", :class=> "signup-wrapper" do %>
<div class="header">
<h2>Create Password</h2>
<p>Fill out the form below to create a new password.</p>
<div class="rg-login-wrapper">
<div class="rg-login-card">
<div class="rg-login-header">
<div class="rg-login-logo">
<i class="bi bi-shield-lock-fill"></i>
</div>
<h2 class="mb-1">Create New Password</h2>
<p class="text-muted mb-0">Choose a strong, unique password</p>
</div>
<div class="content">
<!-- TODO: This form is just a placeholder with no working functionality -->
<%= form_tag "password_resets", html: { class: "needs-validation", novalidate: true } do %>
<%= hidden_field_tag 'user', Base64.encode64(Marshal.dump(@user)) %>
<%= label_tag "Enter Password" %>
<%= 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 class="mb-3">
<label for="password" class="form-label">New Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
<%= password_field_tag :password, params[:password], {
class: "form-control",
id: "password",
placeholder: "Enter new password",
required: true,
autofocus: true,
minlength: 6
} %>
</div>
<div class="form-text">Password must be at least 6 characters long</div>
</div>
<div class="actions">
<%= submit_tag "Create Password", {:class => "btn btn-danger btn-large"} %>
<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="clearfix"></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>
<style>
/* Override main content styling for password reset page */
.rg-main.no-sidebar {
margin: 0;
padding: 0;
}
</style>
+290 -152
View File
@@ -1,145 +1,160 @@
<div class="dashboard-wrapper">
<div class="main-container">
<div class="row-fluid">
<div id="success" style="display: none;" class="alert alert-block alert-success fade in">
<h4 class="alert-heading">
Success!
</h4>
<p>
Information successfully updated.
</p>
<div class="container-fluid">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-bank text-primary"></i> Direct Deposit & Pay
</h2>
<p class="text-muted">Manage your direct deposit accounts and payment settings</p>
</div>
</div>
<div class="row-fluid">
<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">
<%= text_field_tag :bank_account_num, params[:bank_account_num], {:placeholder => "Bank Account Number"} %>
<span class="add-on">#</span>
</div>
<div class="input-append">
<%= text_field_tag :bank_routing_num, params[:bank_routing_num], {:placeholder => "Bank Routing Number"} %>
<span class="add-on">#</span>
<!-- Alert Messages -->
<div class="row">
<div class="col-12">
<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>
<h5 class="alert-heading mb-1">Success!</h5>
<p class="mb-0">Information successfully updated.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div class="input-append">
<%= text_field_tag :dd_percent, params[:dd_percent], {:placeholder => "Percentage of Deposit"} %>
<span class="add-on">%</span>
<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 Inputs -->
<%= submit_tag "Submit", {:id => "dd_form_btn", :style => "margin-left: 10px;", :class => "btn btn-medium btn-primary"} %>
<div class="row g-4">
<!-- Left Column - Forms -->
<div class="col-lg-5">
<!-- Add Direct Deposit Form -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="bi bi-plus-circle text-success me-2"></i>Add Direct Deposit
</h5>
</div>
<div class="card-body p-4">
<%= form_tag "#", { class: "needs-validation", id: "bank_info_form" } do %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-bank2 text-success me-2"></i>Bank Account Number
</label>
<%= text_field_tag :bank_account_num, params[:bank_account_num], {
placeholder: "Enter account number",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Your bank account number</small>
</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>
</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
<!-- 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>
<!-- 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>
<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 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>
<th>Account Number</th>
<th>Routing Number</th>
<th>Deposit %</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- DataTable will populate this -->
</tbody>
</table>
<div class="clearfix">
</div>
</div>
</div> <!-- end of widget body-->
</div>
</div>
</div
<!-- End Dynamic Table ColSpan Table -->
<!-- ###################-->
<!-- Begin Row-Fluid for Decryption Input -->
<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="&#xe11b;"></span> Decrypt Bank Account Number
</div>
</div>
<div class="widget-body">
<div class="row-fluid">
<%= form_tag "#", {:class => "form-horizontal", :id => "decrypt_form" } do %>
<!-- Begin inputs-->
<div class="input-append">
<%= text_field_tag :value_to_decrypt, params[:value_to_decrypt], {:placeholder => "Bank Account Number"} %>
<span class="add-on">#</span>
</div>
<!-- End Inputs -->
<%= submit_tag "Submit", {:id => "decrypt_btn", :style => "margin-left: 10px;", :class => "btn btn-medium btn-primary"} %>
<% end %>
</div>
</div>
</div>
</div>
</div>
<!-- Row-Fluid for Decryption Input -->
</div>
</div>
<%= javascript_include_tag "jquery.dataTables.min.js" %>
@@ -151,9 +166,8 @@
a user to delete that direct deposit entry
*/
function buildDeleteLink(dd_id){
var link = '<a href="/users/' + '<%= current_user.id %>' + '/pay/'+ dd_id + '" data-method="delete" rel="nofollow" class="delete-row">' +
'<i class="icon-trash">'+
'</i></a>'
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="bi bi-trash"></i> Delete</a>'
return link
};
@@ -163,15 +177,38 @@ function buildDeleteLink(dd_id){
*/
function parseDirectDepostInfo(response){
var msg = jQuery.parseJSON(JSON.stringify(response));
var table = $('#data_table').DataTable();
$.each(msg.user, function(index, val){
$('#data_table').dataTable().fnAddData( [
val.bank_account_num,
val.bank_routing_num,
val.percent_of_deposit,
table.row.add( [
'<code class="text-monospace">' + val.bank_account_num + '</code>',
'<span class="badge bg-light text-dark">' + val.bank_routing_num + '</span>',
'<span class="badge bg-success">' + val.percent_of_deposit + '%</span>',
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.
*/
function populateTable() {
$('#data_table').dataTable().fnClearTable();
var table = $('#data_table').DataTable();
table.clear();
$.ajax({
url: <%= sanitize(user_pay_path(:format => "json", user_id: current_user.id, id: current_user.id).inspect) %>,
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
"delete confirmation" message
*/
$('.delete-row').click(function () {
var conf = confirm('Continue delete?');
if (conf) $(this).parents('tr').fadeOut(function () {
$(this).remove();
});
$(document).on('click', '.delete-row', function (e) {
var conf = confirm('Are you sure you want to delete this account?');
if (!conf) {
e.preventDefault();
return false;
}
});
/*
@@ -220,7 +250,27 @@ $('.delete-row').click(function () {
*/
function decryptShow(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').show(500).delay(1500).fadeOut();
decryptShow(response);
$('#decrypt_form')[0].reset();
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
@@ -260,6 +311,7 @@ $("#dd_form_btn").click(function(event) {
type: "POST",
success: function(response) {
$('#success').show(500).delay(1500).fadeOut();
$('#bank_info_form')[0].reset();
populateTable();
},
error: function(event) {
@@ -270,10 +322,27 @@ $("#dd_form_btn").click(function(event) {
$("#encrypted_acct_question").click(function(event) {
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" +
"conveniently located decryption function."
)
// Create modern Bootstrap modal-like alert
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
2) createDataTable - Initializes the dataTable as such
3) populateTable - populates the newly initialized dataTable
Initialize page - called on both ready and turbolinks:load
*/
$(document).ready(
makeActive,
createDataTable(),
populateTable()
)
function initializePage() {
makeActive();
createDataTable();
populateTable();
}
// Handle normal page loads
$(document).ready(function() {
initializePage();
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
initializePage();
});
</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>
+315 -94
View File
@@ -1,43 +1,221 @@
<div class="dashboard-wrapper">
<div class="main-container">
<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="&#xe096;"></span> Performance History Visualization
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-graph-up-arrow text-primary"></i> Performance Reviews
</h2>
<p class="text-muted">Track your performance history and feedback</p>
</div>
</div>
<div class="widget-body">
<div id="line_chart"></div>
<!-- Performance Summary Stats -->
<div class="row g-3 mb-4">
<%
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>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Average Score
</h6>
<h2 class="mb-0" style="color: #579da9; font-weight: 700; font-size: 2.5rem;">
<%= avg_score %>
</h2>
<p class="text-muted mt-2 mb-0 small">Out of 5.0</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="&#xe004;"></span> Performance History
<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 class="widget-body">
<table class="table table-bordered table-striped">
<thead>
</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>
<% else %>
<div class="text-center text-muted py-5">
<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:16%">Reviewer</th>
<th style="width:16%">Date</th>
<th style="width:16%">Score</th>
<th style="width:16%">Comments</th>
<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><%= p.reviewer_name %></td>
<td><%= p.date_submitted %></td>
<td><%= p.score %></td>
<td><%= p.comments %></td>
<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>
<td colspan="4" class="text-center text-muted py-5">
<i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
<p class="mt-3 mb-0">No performance reviews available yet</p>
</td>
</tr>
<% end %>
</tbody>
@@ -50,77 +228,120 @@
</div>
<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(){
$('li[id="performance"]').addClass('active');
};
}
$(document).ready(function() {
drawChart2(),
makeActive()
makeActive();
});
// 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>
+160 -63
View File
@@ -1,84 +1,181 @@
<div class="dashboard-wrapper">
<div class="main-container">
<div class="row-fluid">
<div class="span3"> <!-- Beginning of span-->
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe071;"></span> Employee Contribution
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-piggy-bank-fill text-primary"></i> 401(k) Retirement Plan
</h2>
<p class="text-muted">Your retirement savings summary and employee benefits</p>
</div>
</div>
<div class="widget-body">
<div class="current-statistics">
<div class="expenses">
<h3><%= @info.employee_contrib %></h3>
<!-- Contribution Stats -->
<div class="row g-3 mb-4">
<!-- Employee Contribution -->
<div class="col-lg-4">
<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-person-fill-check" style="font-size: 3rem; color: #579da9;"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Employee Contribution
</h6>
<h2 class="mb-0" style="color: #579da9; font-weight: 700; font-size: 2.5rem;">
<%= @info.employee_contrib %>
</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>
</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
<!-- 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>
<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 class="row-fluid">
<div class="span6"> <!-- Beginning of span-->
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe0d4;"></span> Employee Services
</div>
</div>
<div class="widget-body">
<p>
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 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>
<hr/>
<p>
MetaCorp is dedicated to its employees and this service is just one way of showing it!
<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>
<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> <!-- End of span-->
</div>
</div>
</div>
<script type="text/javascript">
function makeActive() {
$('li[id="retirement"]').addClass('active');
};
}
$(document).ready(makeActive)
$(document).ready(makeActive);
// 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);
}
</style>
+94 -47
View File
@@ -1,55 +1,102 @@
<div align="right">
<!-- support for multiple languages coming soon! -->
<div class="rg-login-wrapper">
<div class="rg-login-card">
<div class="rg-login-header">
<div class="rg-login-logo">
<i class="bi bi-shield-fill-exclamation"></i>
</div>
<h2 class="mb-1">MetaCorp</h2>
<p class="text-muted mb-0">A GoatGroup Company</p>
</div>
<%= form_tag "sessions", class: "needs-validation", novalidate: true do %>
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
<%= text_field_tag :email, params[:email], {
class: "form-control",
id: "email",
placeholder: "you@example.com",
required: true,
autofocus: true
} %>
</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>
<!-- VULNERABILITY: XSS via URL hash parameter -->
<script>
//document.write("<select style=\"width: 100px;\">");
//document.write("<OPTION value=1>English</OPTION>");
//document.write("<OPTION value=2>Spanish</OPTION>");
// 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]);
document.write("<OPTION value=3>" + paramValue + "</OPTION>");
} catch(err) {
// VULNERABLE: Directly writing user input to DOM
document.write("<div class='alert alert-info mt-3'>" + paramValue + "</div>");
}
} catch(err) {
// Silently fail
}
//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=> "signup-wrapper" do %>
<div class="header">
<h2>Login</h2>
<p>Fill out the form below to login to your control panel.</p>
</div>
<div class="content">
<%= hidden_field_tag :url, @url %>
<%= text_field_tag :email, params[:email], {:class => "input input-block-level", :placeholder=>"Email"} %>
<%= 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>
</div>
<style>
/* Override main content styling for login page */
.rg-main.no-sidebar {
margin: 0;
padding: 0;
}
</style>
+49 -85
View File
@@ -1,115 +1,79 @@
<!-- Begin Modal -->
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">
×
</button>
<h4 id="myModalLabel1">
Application Credentials (Spoiler)
<div class="container mt-4">
<div class="card">
<div class="card-header bg-warning text-dark d-flex justify-content-between align-items-center">
<h4 class="mb-0">
<i class="bi bi-key"></i> Application Credentials (Spoiler)
</h4>
<%= link_to root_path, class: "btn btn-sm btn-outline-dark" do %>
<i class="bi bi-x-lg"></i> Close
<% end %>
</div>
<div class="modal-body">
<div class="row">
<div class="span8">
<p>Warning, this is a spoiler</p>
<p>Are you sure you want to see the credentials?</p>
<div class="card-body">
<div class="alert alert-warning" role="alert">
<i class="bi bi-exclamation-triangle"></i>
<strong>Warning:</strong> This is a spoiler. Are you sure you want to see the credentials?
</div>
<div id="creds_hidden" style="display:none">
<table class="table table-striped table-hover table-bordered pull-left" id="data-table">
<table class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>
Email
</th>
<th>
Password
</th>
<th>
API Key
</th>
<th>Email</th>
<th>Password</th>
<th>API Key</th>
</tr>
</thead>
<tbody>
<tr>
<td style="word-wrap:break-word;">
admin@metacorp.com
</td>
<td>
admin1234
</td>
<td>
1-01de24d75cffaa66db205278d1cf900bf087a737
</td>
<td style="word-wrap:break-word;">admin@metacorp.com</td>
<td>admin1234</td>
<td>1-01de24d75cffaa66db205278d1cf900bf087a737</td>
</tr>
<tr>
<td style="word-wrap:break-word;">
jmmastey@metacorp.com
</td>
<td>
railsgoat!
</td>
<td>
2-050ddd40584978fe9e82840b8b95abb98e4786dc
</td>
<td style="word-wrap:break-word;">jmmastey@metacorp.com</td>
<td>railsgoat!</td>
<td>2-050ddd40584978fe9e82840b8b95abb98e4786dc</td>
</tr>
<tr>
<td style="word-wrap:break-word;">
jim@metacorp.com
</td>
<td>
alohaowasp
</td>
<td>
3-eaa9b4d748d6a8c6a38e24ac1cc2204ebc3541c1
</td>
<td style="word-wrap:break-word;">jim@metacorp.com</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>
<td style="word-wrap:break-word;">mike@metacorp.com</td>
<td>motocross1445</td>
<td>4-c809b3d11d272cff8cab1da9e4cdf61137f29d2</td>
</tr>
<tr>
<td style="word-wrap:break-word;">
ken@metacorp.com
</td>
<td>
citrusblend
</td>
<td>
5-4af604a848ca212cfa3935352aabe9522cf89fdc
</td>
<td style="word-wrap:break-word;">ken@metacorp.com</td>
<td>citrusblend</td>
<td>5-4af604a848ca212cfa3935352aabe9522cf89fdc</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="row-fluid">
</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="text-center mt-3">
<button id="understood" class="btn btn-primary">
<i class="bi bi-eye"></i> I understand - Show Credentials
</button>
</div>
</div>
<!-- End Modal -->
<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>
<script type="text/javascript">
$(document).ready(function() {
$('#understood').click(function() {
$("#creds_hidden").show();
$(this).hide();
});
});
</script>
+106 -23
View File
@@ -1,36 +1,119 @@
<div class="row-fluid">
<div class="span12">
<div class="rg-login-wrapper">
<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">
<div class="span4 offset4">
<div class="signup">
<%= form_for @user, :html => {:id => "account_edit", :class=> "signup-wrapper"} do |f| %>
<div class="header">
<h2>Sign Up</h2>
<p>Fill out the form below to login</p>
</div>
<div class="content">
<%= f.text_field :email, {:class => "input input-block-level", :placeholder => "Email"} %>
<%= f.text_field :first_name, {:class => "input input-block-level", :placeholder => "First Name"} %>
<%= f.text_field :last_name, {:class => "input input-block-level", :placeholder => "Last Name"} %>
<div class="control-group">
<%= f.password_field :password, {:class => "input input-block-level", :placeholder => "Password (at least six characters)"}%>
</div>
<div class="control-group">
<%= f.password_field :password_confirmation, {:class => "input input-block-level", :placeholder => "Confirm Password"}%>
<%= form_for @user, html: { id: "account_edit", class: "needs-validation", novalidate: true } do |f| %>
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
<%= f.text_field :email, {
class: "form-control",
id: "email",
placeholder: "you@example.com",
required: true,
autofocus: true
} %>
</div>
</div>
<div class="actions">
<%= f.submit "Submit", {:id => 'submit_button', :class => "btn btn-info btn-large pull-right"} %>
<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 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>
<div class="clearfix"></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>
<%= 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
collection do
get "credentials"
post "redos_email"
post "redos_username"
post "redos_email_safe"
get "supply_chain"
get "check_dependencies"
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
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# 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|
t.string "ip_address"
t.string "referrer"
@@ -113,5 +111,4 @@ ActiveRecord::Schema.define(version: 20171007010129) do
t.datetime "updated_at"
t.binary "encrypted_ssn"
end
end
+21 -21
View File
@@ -23,21 +23,21 @@ users = [
},
{
email: "jack@metacorp.com",
email: "john@metacorp.com",
admin: false,
password: "yankeessuck",
password_confirmation: "yankeessuck",
first_name: "Jack",
last_name: "Mannino",
first_name: "John",
last_name: "Smith",
},
{
email: "jim@metacorp.com",
email: "james@metacorp.com",
admin: false,
password: "alohaowasp",
password_confirmation: "alohaowasp",
first_name: "Jim",
last_name: "Manico",
first_name: "James",
last_name: "Anderson",
},
{
@@ -70,13 +70,13 @@ users = [
retirements = [
{
user: "jack@metacorp.com",
user: "john@metacorp.com",
employee_contrib: "1000",
employer_contrib: "2000",
total: "4500"
},
{
user: "jim@metacorp.com",
user: "james@metacorp.com",
employee_contrib: "8000",
employer_contrib: "16000",
total: "30000"
@@ -97,14 +97,14 @@ retirements = [
paid_time_off = [
{
user: "jack@metacorp.com",
user: "john@metacorp.com",
sick_days_taken: 2,
sick_days_earned: 5,
pto_taken: 5,
pto_earned: 30
},
{
user: "jim@metacorp.com",
user: "james@metacorp.com",
sick_days_taken: 3,
sick_days_earned: 6,
pto_taken: 3,
@@ -128,7 +128,7 @@ paid_time_off = [
schedule = [
{
user: "jack@metacorp.com",
user: "john@metacorp.com",
date_begin: Date.new(2014, 7, 30),
date_end: Date.new(2014, 8, 2),
event_type: "pto",
@@ -136,7 +136,7 @@ schedule = [
event_name: "My 2014 Vacation"
},
{
user: "jim@metacorp.com",
user: "james@metacorp.com",
date_begin: Date.new(2013, 9, 1),
date_end: Date.new(2013, 9, 12),
event_type: "pto",
@@ -163,7 +163,7 @@ schedule = [
work_info = [
{
user: "jack@metacorp.com",
user: "john@metacorp.com",
income: "$50,000",
bonuses: "$10,000",
years_worked: 2,
@@ -171,7 +171,7 @@ work_info = [
DoB: "01-01-1980"
},
{
user: "jim@metacorp.com",
user: "james@metacorp.com",
income: "$40,000",
bonuses: "$10,000",
years_worked: 1,
@@ -198,21 +198,21 @@ work_info = [
performance = [
{
user: "jack@metacorp.com",
user: "john@metacorp.com",
reviewer: 1,
comments: "Great job! You are my hero",
date_submitted: Date.new(2012, 01, 01),
score: 5
},
{
user: "jack@metacorp.com",
user: "john@metacorp.com",
reviewer: 1,
comments: "Once again, you've done a great job this year. We greatly appreciate your hard work.",
date_submitted: Date.new(2013, 01, 01),
score: 5
},
{
user: "jim@metacorp.com",
user: "james@metacorp.com",
reviewer: 1,
comments: "Great worker, great attitude for this newcomer!",
date_submitted: Date.new(2013, 01, 01),
@@ -251,24 +251,24 @@ performance = [
messages = [
{
creator: "ken@metacorp.com",
receiver: "jack@metacorp.com",
receiver: "john@metacorp.com",
message: "Your benefits have been updated.",
read: false
},
{
creator: "mike@metacorp.com",
receiver: "jim@metacorp.com",
receiver: "james@metacorp.com",
message: "Please update your profile.",
read: false
},
{
creator: "jim@metacorp.com",
creator: "james@metacorp.com",
receiver: "mike@metacorp.com",
message: "Welcome to Railsgoat.",
read: false
},
{
creator: "jack@metacorp.com",
creator: "john@metacorp.com",
receiver: "ken@metacorp.com",
message: "Hello friend.",
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 "rspec/rails"
require "capybara/rails"
require "capybara/poltergeist"
require "selenium-webdriver"
require "database_cleaner"
# 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!
end
Capybara.javascript_driver = :poltergeist
Capybara.javascript_driver = :selenium_headless
DatabaseCleaner.strategy = :truncation