Compare commits
16 Commits
9acdc4e6db
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b6e059da6 | ||
|
|
e9f8f03db2 | ||
|
|
b289cdc320 | ||
|
|
8bb410dcfa | ||
|
|
ce67776eec | ||
|
|
9f71fe65e5 | ||
|
|
e15835bda9 | ||
|
|
83320d4c9a | ||
|
|
a2008e2ae3 | ||
|
|
b45a451748 | ||
|
|
441caabb98 | ||
|
|
a139bde102 | ||
|
|
f35a09f07a | ||
|
|
1a10e3c784 | ||
|
|
4fe95ca538 | ||
|
|
fa36305244 |
@@ -0,0 +1 @@
|
||||
markup: markdown
|
||||
@@ -18,6 +18,8 @@ gem "turbo-rails"
|
||||
gem "stimulus-rails"
|
||||
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
|
||||
gem "jbuilder"
|
||||
gem "grape"
|
||||
gem "rswag-ui"
|
||||
gem "caxlsx"
|
||||
gem "caxlsx_rails"
|
||||
|
||||
@@ -44,6 +46,9 @@ gem "thruster", require: false
|
||||
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
|
||||
gem "image_processing", "~> 1.2"
|
||||
|
||||
# Pagination [https://github.com/ddnexus/pagy]
|
||||
gem "pagy", "~> 8.0"
|
||||
|
||||
group :development, :test do
|
||||
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
||||
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
|
||||
@@ -56,6 +61,9 @@ group :development, :test do
|
||||
|
||||
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
|
||||
gem "rubocop-rails-omakase", require: false
|
||||
|
||||
# Parallel test runner [https://github.com/grosser/parallel_tests]
|
||||
gem "parallel_tests", require: false
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
||||
@@ -84,9 +84,9 @@ GEM
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.0.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.21.1)
|
||||
bootsnap (1.23.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (8.0.1)
|
||||
brakeman (8.0.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bundler-audit (0.9.3)
|
||||
@@ -120,6 +120,26 @@ GEM
|
||||
docile (1.4.1)
|
||||
dotenv (3.2.0)
|
||||
drb (2.2.3)
|
||||
dry-configurable (1.3.0)
|
||||
dry-core (~> 1.1)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-core (1.2.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
logger
|
||||
zeitwerk (~> 2.6)
|
||||
dry-inflector (1.3.1)
|
||||
dry-logic (1.6.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.1)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-types (1.9.1)
|
||||
bigdecimal (>= 3.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0)
|
||||
dry-inflector (~> 1.0)
|
||||
dry-logic (~> 1.4)
|
||||
zeitwerk (~> 2.6)
|
||||
ed25519 (1.4.0)
|
||||
erb (6.0.1)
|
||||
erubi (1.13.1)
|
||||
@@ -131,6 +151,13 @@ GEM
|
||||
raabro (~> 1.4)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
grape (3.1.1)
|
||||
activesupport (>= 7.1)
|
||||
dry-configurable
|
||||
dry-types (>= 1.1)
|
||||
mustermann-grape (~> 1.1.0)
|
||||
rack (>= 2)
|
||||
zeitwerk
|
||||
htmlentities (4.4.2)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
@@ -142,14 +169,15 @@ GEM
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
io-console (0.8.2)
|
||||
irb (1.16.0)
|
||||
irb (1.17.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jbuilder (2.14.1)
|
||||
actionview (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
json (2.18.0)
|
||||
json (2.18.1)
|
||||
kamal (2.10.1)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
@@ -181,7 +209,11 @@ GEM
|
||||
minitest (6.0.1)
|
||||
prism (~> 1.5)
|
||||
msgpack (1.8.0)
|
||||
net-imap (0.6.2)
|
||||
mustermann (3.0.4)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
mustermann-grape (1.1.0)
|
||||
mustermann (>= 1.0.0)
|
||||
net-imap (0.6.3)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -196,11 +228,14 @@ GEM
|
||||
net-protocol
|
||||
net-ssh (7.3.0)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.0-x86_64-linux-gnu)
|
||||
nokogiri (1.19.1-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
ostruct (0.6.3)
|
||||
pagy (8.6.3)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.1)
|
||||
parallel_tests (5.6.0)
|
||||
parallel
|
||||
parser (3.3.10.2)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pp (0.6.3)
|
||||
@@ -219,7 +254,7 @@ GEM
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.4)
|
||||
rack (3.2.5)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
@@ -259,7 +294,7 @@ GEM
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.1)
|
||||
rdoc (7.1.0)
|
||||
rdoc (7.2.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
@@ -273,7 +308,10 @@ GEM
|
||||
logger (~> 1)
|
||||
nokogiri (~> 1)
|
||||
rubyzip (>= 3.0.0, < 4.0.0)
|
||||
rubocop (1.84.0)
|
||||
rswag-ui (2.17.0)
|
||||
actionpack (>= 5.2, < 8.2)
|
||||
railties (>= 5.2, < 8.2)
|
||||
rubocop (1.84.2)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -305,9 +343,10 @@ GEM
|
||||
ruby-vips (2.3.0)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (3.2.2)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.40.0)
|
||||
selenium-webdriver (4.41.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
@@ -328,7 +367,7 @@ GEM
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
solid_queue (1.3.1)
|
||||
solid_queue (1.3.2)
|
||||
activejob (>= 7.1)
|
||||
activerecord (>= 7.1)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
@@ -347,7 +386,7 @@ GEM
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.2.0)
|
||||
thor (1.5.0)
|
||||
thruster (0.1.17-x86_64-linux)
|
||||
thruster (0.1.18-x86_64-linux)
|
||||
timeout (0.6.0)
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.23)
|
||||
@@ -372,7 +411,7 @@ GEM
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.4)
|
||||
zeitwerk (2.7.5)
|
||||
|
||||
PLATFORMS
|
||||
x86_64-linux-gnu
|
||||
@@ -387,14 +426,18 @@ DEPENDENCIES
|
||||
caxlsx
|
||||
caxlsx_rails
|
||||
debug
|
||||
grape
|
||||
image_processing (~> 1.2)
|
||||
importmap-rails
|
||||
jbuilder
|
||||
kamal
|
||||
pagy (~> 8.0)
|
||||
parallel_tests
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
rails (~> 8.1.2)
|
||||
roo (~> 3.0)
|
||||
rswag-ui
|
||||
rubocop-rails-omakase
|
||||
selenium-webdriver
|
||||
simplecov
|
||||
@@ -429,8 +472,8 @@ CHECKSUMS
|
||||
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
|
||||
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
|
||||
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
|
||||
bootsnap (1.21.1) sha256=9373acfe732da35846623c337d3481af8ce77c7b3a927fb50e9aa92b46dbc4c4
|
||||
brakeman (8.0.1) sha256=c68ce0ac35a6295027c4eab8b4ac597d2a0bfc82f0d62dcd334bbf944d352f70
|
||||
bootsnap (1.23.0) sha256=c1254f458d58558b58be0f8eb8f6eec2821456785b7cdd1e16248e2020d3f214
|
||||
brakeman (8.0.2) sha256=7b02065ce8b1de93949cefd3f2ad78e8eb370e644b95c8556a32a912a782426a
|
||||
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
|
||||
bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
|
||||
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
|
||||
@@ -445,6 +488,11 @@ CHECKSUMS
|
||||
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
||||
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
|
||||
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
||||
dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8
|
||||
dry-core (1.2.0) sha256=0cc5a7da88df397f153947eeeae42e876e999c1e30900f3c536fb173854e96a1
|
||||
dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc
|
||||
dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2
|
||||
dry-types (1.9.1) sha256=baebeecdb9f8395d6c9d227b62011279440943e3ef2468fe8ccc1ba11467f178
|
||||
ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506
|
||||
erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
|
||||
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
|
||||
@@ -452,14 +500,15 @@ CHECKSUMS
|
||||
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
|
||||
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
|
||||
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
|
||||
grape (3.1.1) sha256=774f16782d917a90e69de0499dfaab571e5ad967569ac066a2b0b918af12de69
|
||||
htmlentities (4.4.2) sha256=bbafbdf69f2eca9262be4efef7e43e6a1de54c95eb600f26984f71d2fe96c5c3
|
||||
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
|
||||
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
|
||||
importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a
|
||||
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
||||
irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806
|
||||
irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
|
||||
jbuilder (2.14.1) sha256=4eb26376ff60ef100cb4fd6fd7533cd271f9998327e86adf20fd8c0e69fabb42
|
||||
json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
|
||||
json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
|
||||
kamal (2.10.1) sha256=53b7ecb4c33dd83b1aedfc7aacd1c059f835993258a552d70d584c6ce32b6340
|
||||
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
||||
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
||||
@@ -472,7 +521,9 @@ CHECKSUMS
|
||||
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
|
||||
minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb
|
||||
msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732
|
||||
net-imap (0.6.2) sha256=08caacad486853c61676cca0c0c47df93db02abc4a8239a8b67eb0981428acc6
|
||||
mustermann (3.0.4) sha256=85fadcb6b3c6493a8b511b42426f904b7f27b282835502233dd154daab13aa22
|
||||
mustermann-grape (1.1.0) sha256=8d258a986004c8f01ce4c023c0b037c168a9ed889cf5778068ad54398fa458c5
|
||||
net-imap (0.6.3) sha256=9bab75f876596d09ee7bf911a291da478e0cd6badc54dfb82874855ccc82f2ad
|
||||
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
|
||||
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
|
||||
net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d
|
||||
@@ -480,10 +531,12 @@ CHECKSUMS
|
||||
net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
|
||||
net-ssh (7.3.0) sha256=172076c4b30ce56fb25a03961b0c4da14e1246426401b0f89cba1a3b54bf3ef0
|
||||
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
|
||||
nokogiri (1.19.0-x86_64-linux-gnu) sha256=f482b95c713d60031d48c44ce14562f8d2ce31e3a9e8dd0ccb131e9e5a68b58c
|
||||
nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a
|
||||
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
|
||||
pagy (8.6.3) sha256=537b2ee3119f237dd6c4a0d0a35c67a77b9d91ebb9d4f85e31407c2686774fb2
|
||||
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
|
||||
parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688
|
||||
parallel_tests (5.6.0) sha256=b2d7af382d1c0289daba65308d9143b3ad82d817d500c0ad1b4bde468beb13af
|
||||
parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
|
||||
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
|
||||
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
||||
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
||||
@@ -493,7 +546,7 @@ CHECKSUMS
|
||||
puma (7.2.0) sha256=bf8ef4ab514a4e6d4554cb4326b2004eba5036ae05cf765cfe51aba9706a72a8
|
||||
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
|
||||
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
||||
rack (3.2.4) sha256=5d74b6f75082a643f43c1e76b419c40f0e5527fcfee1e669ac1e6b73c0ccb6f6
|
||||
rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3
|
||||
rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9
|
||||
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
|
||||
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
||||
@@ -503,33 +556,35 @@ CHECKSUMS
|
||||
railties (8.1.2) sha256=1289ece76b4f7668fc46d07e55cc992b5b8751f2ad85548b7da351b8c59f8055
|
||||
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
||||
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
|
||||
rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363
|
||||
rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
|
||||
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
|
||||
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
||||
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
||||
roo (3.0.0) sha256=6fdd7a9158d657c69768b4168754ff2110cc21fdc01a1bec1010820cb05c91b1
|
||||
rubocop (1.84.0) sha256=88dec310153bb685a879f5a7cdb601f6287b8f0ee675d9dc63a17c7204c4190a
|
||||
rswag-ui (2.17.0) sha256=5f707b9b5e8171ddf9f519f6e401e79e419bd1d07387508603e76124f2443212
|
||||
rubocop (1.84.2) sha256=5692cea54168f3dc8cb79a6fe95c5424b7ea893c707ad7a4307b0585e88dbf5f
|
||||
rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
|
||||
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
|
||||
rubocop-rails (2.34.3) sha256=10d37989024865ecda8199f311f3faca990143fbac967de943f88aca11eb9ad2
|
||||
rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
|
||||
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
||||
ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374
|
||||
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
|
||||
rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c
|
||||
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
||||
selenium-webdriver (4.40.0) sha256=16ef7aa9853c1d4b9d52eac45aafa916e3934c5c83cb4facb03f250adfd15e5b
|
||||
selenium-webdriver (4.41.0) sha256=cdc1173cd55cf186022cea83156cc2d0bec06d337e039b02ad25d94e41bedd22
|
||||
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
|
||||
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
|
||||
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
|
||||
solid_cable (3.0.12) sha256=a168a54731a455d5627af48d8441ea3b554b8c1f6e6cd6074109de493e6b0460
|
||||
solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41
|
||||
solid_queue (1.3.1) sha256=d9580111180c339804ff1a810a7768f69f5dc694d31e86cf1535ff2cd7a87428
|
||||
solid_queue (1.3.2) sha256=44a53047be4255f616ff13fa5d35980e7b3eee6e31d957eadb88fbe8e0db4509
|
||||
sqlite3 (2.9.0-x86_64-linux-gnu) sha256=72fff9bd750070ba3af695511ba5f0e0a2d8a9206f84869640b3e99dfaf3d5a5
|
||||
sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744
|
||||
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
|
||||
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
|
||||
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
||||
thruster (0.1.17-x86_64-linux) sha256=77b8f335075bd4ece7631dc84a19a710a1e6e7102cbce147b165b45851bdfcd3
|
||||
thruster (0.1.18-x86_64-linux) sha256=0ec1ff5f12289c1ac10cf8e28ce6b5266f4e73416b34a664b79d037c7d955c40
|
||||
timeout (0.6.0) sha256=6d722ad619f96ee383a0c557ec6eb8c4ecb08af3af62098a0be5057bf00de1af
|
||||
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
||||
turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f
|
||||
@@ -543,7 +598,7 @@ CHECKSUMS
|
||||
websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
|
||||
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
|
||||
xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e
|
||||
zeitwerk (2.7.4) sha256=2bef90f356bdafe9a6c2bd32bcd804f83a4f9b8bc27f3600fff051eb3edcec8b
|
||||
zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
|
||||
|
||||
BUNDLED WITH
|
||||
4.0.4
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
"Sanasto Wiki" is a web-based dictionary application for simultaneous translators in the living Christianity. The application provides publicly accessible translations while restricting editing and commenting to invited contributors.
|
||||
"Sanasto Wiki" is a web-based glossary for simultaneous translators in the living Christianity. The application provides publicly accessible translations while restricting editing and commenting to invited contributors.
|
||||
|
||||
---
|
||||
|
||||
@@ -188,15 +188,25 @@ See 'public/Kristillisyyden sanasto ver 23.5.2013.xlsx'
|
||||
|
||||
---
|
||||
|
||||
## API (Optional Future)
|
||||
## API (Public Sync)
|
||||
|
||||
REST API for potential mobile app or integration:
|
||||
Public JSON endpoint for syncing entries:
|
||||
```
|
||||
GET /api/entries
|
||||
GET /api/entries/:id
|
||||
GET /api/entries/search?q=:query&lang=:code
|
||||
POST /api/entries (authenticated)
|
||||
PATCH /api/entries/:id (authenticated)
|
||||
GET /api/entries
|
||||
GET /api/entries?since=2026-01-01T12:00:00Z
|
||||
```
|
||||
|
||||
Responses include all language columns, category and `updated_at`. The optional `since`
|
||||
parameter filters by `updated_at` (ISO8601).
|
||||
|
||||
Swagger docs:
|
||||
```
|
||||
GET /api/swagger
|
||||
```
|
||||
|
||||
Swagger UI:
|
||||
```
|
||||
GET /api
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
require "grape"
|
||||
require "ostruct"
|
||||
|
||||
class Api::Base < Grape::API
|
||||
format :json
|
||||
default_format :json
|
||||
content_type :json, "application/json"
|
||||
|
||||
helpers do
|
||||
def parse_since_param(raw_since)
|
||||
return nil if raw_since.blank?
|
||||
|
||||
Time.iso8601(raw_since)
|
||||
rescue ArgumentError
|
||||
error!({ error: "Invalid since parameter. Use ISO8601 timestamp." }, 400)
|
||||
end
|
||||
end
|
||||
|
||||
resource :entries do
|
||||
desc "Return public entries in all languages",
|
||||
attributes: OpenStruct.new(success: nil, produces: nil)
|
||||
params do
|
||||
optional :since,
|
||||
type: String,
|
||||
desc: "ISO8601 timestamp. Returns entries updated after this time."
|
||||
end
|
||||
get do
|
||||
since_time = parse_since_param(params[:since])
|
||||
|
||||
entries_scope = Entry.active_entries
|
||||
entries_scope = entries_scope.where("updated_at > ?", since_time) if since_time
|
||||
|
||||
entries_scope
|
||||
.order(:updated_at, :id)
|
||||
.select(
|
||||
:id,
|
||||
:category,
|
||||
:fi,
|
||||
:en,
|
||||
:sv,
|
||||
:no,
|
||||
:ru,
|
||||
:de,
|
||||
:updated_at
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,119 @@
|
||||
# config/routes.rb
|
||||
|
||||
# app/controllers/api/swagger_controller.rb
|
||||
module Api
|
||||
class SwaggerController < ApplicationController
|
||||
def index
|
||||
render json: {
|
||||
openapi: "3.0.0",
|
||||
info: {
|
||||
title: "Sanasto Wiki API",
|
||||
description: "Public sync API for Sanasto Wiki glossary entries.",
|
||||
version: "1.0.0"
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: "https://#{request.host}",
|
||||
description: "Production server"
|
||||
}
|
||||
],
|
||||
paths: {
|
||||
"/api/entries": {
|
||||
get: {
|
||||
summary: "Return public entries in all languages",
|
||||
description: "Retrieve all active glossary entries with optional filtering by update timestamp",
|
||||
tags: [ "Entries" ],
|
||||
parameters: [
|
||||
{
|
||||
name: "since",
|
||||
in: "query",
|
||||
description: "ISO8601 timestamp. Returns entries updated after this time.",
|
||||
required: false,
|
||||
schema: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
example: "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
responses: {
|
||||
"200": {
|
||||
description: "List of entries",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
"$ref": "#/components/schemas/Entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
description: "Invalid since parameter",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: { type: "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
Entry: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "integer",
|
||||
description: "Entry ID"
|
||||
},
|
||||
category: {
|
||||
type: "string",
|
||||
description: "Entry category"
|
||||
},
|
||||
fi: {
|
||||
type: "string",
|
||||
description: "Finnish translation"
|
||||
},
|
||||
en: {
|
||||
type: "string",
|
||||
description: "English translation"
|
||||
},
|
||||
sv: {
|
||||
type: "string",
|
||||
description: "Swedish translation"
|
||||
},
|
||||
no: {
|
||||
type: "string",
|
||||
description: "Norwegian translation"
|
||||
},
|
||||
ru: {
|
||||
type: "string",
|
||||
description: "Russian translation"
|
||||
},
|
||||
de: {
|
||||
type: "string",
|
||||
description: "German translation"
|
||||
},
|
||||
updated_at: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
description: "Last update timestamp (ISO8601)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,6 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include BotBlocker
|
||||
include Pagy::Backend
|
||||
|
||||
# Changes to the importmap will invalidate the etag for HTML responses
|
||||
stale_when_importmap_changes
|
||||
|
||||
@@ -6,8 +6,6 @@ class EntriesController < ApplicationController
|
||||
@category = params[:category].presence
|
||||
@query = params[:q].to_s.strip
|
||||
@starts_with = params[:starts_with].presence
|
||||
@page = [ params[:page].to_i, 1 ].max
|
||||
@per_page = 25
|
||||
|
||||
entries_scope = Entry.active_entries
|
||||
entries_scope = entries_scope.with_category(@category)
|
||||
@@ -16,9 +14,8 @@ class EntriesController < ApplicationController
|
||||
entries_scope = entries_scope.alphabetical_for(@language_code) if @query.blank? && @starts_with.blank? && @language_code.present?
|
||||
entries_scope = entries_scope.order(created_at: :desc) if entries_scope.order_values.empty?
|
||||
|
||||
@total_entries = entries_scope.count
|
||||
@total_pages = (@total_entries.to_f / @per_page).ceil
|
||||
@entries = entries_scope.offset((@page - 1) * @per_page).limit(@per_page)
|
||||
@pagy, @entries = pagy(entries_scope, items: 25)
|
||||
@total_entries = @pagy.count
|
||||
|
||||
@entry_count = Entry.active_entries.count
|
||||
@requested_count = Entry.requested.count
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
module ApplicationHelper
|
||||
include Pagy::Frontend
|
||||
|
||||
def language_name(code)
|
||||
supported_languages.find { |l| l.code == code }&.name
|
||||
end
|
||||
|
||||
@@ -86,17 +86,19 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-4 text-sm text-slate-600">
|
||||
<div>Page <%= @page %> of <%= [@total_pages, 1].max %></div>
|
||||
<div><%= pagy_info(@pagy).html_safe %></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<% previous_page = @page > 1 ? @page - 1 : nil %>
|
||||
<% next_page = @page < @total_pages ? @page + 1 : nil %>
|
||||
<% pagination_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact %>
|
||||
<%= link_to "Previous", previous_page ? entries_path(pagination_params.merge(page: previous_page)) : "#",
|
||||
class: "px-3 py-1.5 rounded-md border border-slate-200 #{previous_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}",
|
||||
data: { turbo_stream: true } %>
|
||||
<%= link_to "Next", next_page ? entries_path(pagination_params.merge(page: next_page)) : "#",
|
||||
class: "px-3 py-1.5 rounded-md border border-slate-200 #{next_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}",
|
||||
data: { turbo_stream: true } %>
|
||||
<%
|
||||
pagination_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact
|
||||
prev_url = @pagy.prev ? entries_path(pagination_params.merge(page: @pagy.prev)) : nil
|
||||
next_url = @pagy.next ? entries_path(pagination_params.merge(page: @pagy.next)) : nil
|
||||
%>
|
||||
<%= link_to "Previous", prev_url || "#",
|
||||
class: "px-3 py-1.5 rounded-md border border-slate-200 #{'opacity-50 pointer-events-none' unless prev_url}",
|
||||
data: (prev_url ? { turbo_stream: true } : {}) %>
|
||||
<%= link_to "Next", next_url || "#",
|
||||
class: "px-3 py-1.5 rounded-md border border-slate-200 #{'opacity-50 pointer-events-none' unless next_url}",
|
||||
data: (next_url ? { turbo_stream: true } : {}) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="/icon.png" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/icon.png">
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="apple-touch-icon" href="/icon-512.png" />
|
||||
|
||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"name": "SanastoWiki",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"src": "/icon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
},
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"src": "/icon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require_relative "boot"
|
||||
|
||||
require "rails/all"
|
||||
require_relative "../lib/middleware/sanasto_cors"
|
||||
|
||||
# Require the gems listed in Gemfile, including any gems
|
||||
# you've limited to :test, :development, or :production.
|
||||
@@ -24,5 +25,7 @@ module SanastoWiki
|
||||
# config.time_zone = "Central Time (US & Canada)"
|
||||
# config.eager_load_paths << Rails.root.join("extras")
|
||||
config.active_record.schema_format = :sql
|
||||
|
||||
config.middleware.insert_before 0, Middleware::SanastoCors
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ development:
|
||||
# Do not set this db to the same as development or production.
|
||||
test:
|
||||
<<: *default
|
||||
database: storage/test.sqlite3
|
||||
database: storage/test<%= ENV["TEST_ENV_NUMBER"] %>.sqlite3
|
||||
|
||||
# Store production database in the storage/ directory, which by default
|
||||
# is mounted as a persistent Docker volume in config/deploy.yml.
|
||||
|
||||
@@ -17,7 +17,9 @@ servers:
|
||||
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
|
||||
proxy:
|
||||
ssl: true
|
||||
host: sanasto.rin.no
|
||||
hosts:
|
||||
- sanasto.rin.no
|
||||
- sanasto.wiki
|
||||
# Kamal proxy will forward to your app on port 3000
|
||||
|
||||
# Credentials for your image host.
|
||||
|
||||
@@ -37,6 +37,7 @@ Rails.application.configure do
|
||||
# Make template changes take effect immediately.
|
||||
config.action_mailer.perform_caching = false
|
||||
|
||||
# You can use Mailpit for SMTP in development - https://github.com/axllent/mailpit
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
config.action_mailer.smtp_settings = {
|
||||
address: "localhost",
|
||||
|
||||
@@ -58,7 +58,7 @@ Rails.application.configure do
|
||||
# config.action_mailer.raise_delivery_errors = false
|
||||
|
||||
# Set host to be used by links generated in mailer templates.
|
||||
config.action_mailer.default_url_options = { host: "sanasto.rin.no" }
|
||||
config.action_mailer.default_url_options = { host: "sanasto.wiki" }
|
||||
|
||||
# Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
|
||||
config.action_mailer.smtp_settings = {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Pagy Configuration
|
||||
require "pagy/extras/overflow"
|
||||
|
||||
Pagy::DEFAULT[:items] = 25 # Match current 25 items per page
|
||||
Pagy::DEFAULT[:page_param] = :page
|
||||
Pagy::DEFAULT[:overflow] = :last_page
|
||||
@@ -0,0 +1,3 @@
|
||||
Rswag::Ui.configure do |config|
|
||||
config.swagger_endpoint "/api/swagger", "Sanasto Wiki API"
|
||||
end
|
||||
@@ -1,6 +1,10 @@
|
||||
Rails.application.routes.draw do
|
||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||
|
||||
mount Api::Base => "/api"
|
||||
get "/api/swagger", to: "api/swagger#index"
|
||||
mount Rswag::Ui::Engine => "/api"
|
||||
|
||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
||||
get "up" => "rails/health#show", as: :rails_health_check
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Deployment (Kamal)
|
||||
|
||||
We deploy with Kamal. You do NOT need to manually set up web servers or Docker
|
||||
on the VM. `kamal setup` provisions everything.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Local machine
|
||||
- Ruby + Bundler
|
||||
- Docker (for building images)
|
||||
- SSH key access to the VM
|
||||
- Registry credentials (see `config/deploy.yml`)
|
||||
|
||||
### Remote VM
|
||||
- Bare VM with SSH access
|
||||
- Open ports: 22, 80, 443
|
||||
- A domain name pointing at the VM (for SSL)
|
||||
|
||||
## Configure
|
||||
|
||||
Update `config/deploy.yml`:
|
||||
|
||||
```yaml
|
||||
service: sanasto-wiki
|
||||
image: your-registry/sanasto-wiki
|
||||
|
||||
servers:
|
||||
web:
|
||||
- your-server-ip
|
||||
|
||||
proxy:
|
||||
ssl: true
|
||||
host: sanasto.example.com
|
||||
|
||||
registry:
|
||||
server: ghcr.io
|
||||
username: your-github-username
|
||||
|
||||
ssh:
|
||||
user: deploy
|
||||
```
|
||||
|
||||
Set the registry password:
|
||||
|
||||
```bash
|
||||
export KAMAL_REGISTRY_PASSWORD="your-token"
|
||||
```
|
||||
|
||||
Make sure `config/master.key` is present locally.
|
||||
|
||||
## First deploy
|
||||
|
||||
```bash
|
||||
bundle exec kamal setup
|
||||
```
|
||||
|
||||
## Regular deploys
|
||||
|
||||
```bash
|
||||
bundle exec kamal deploy
|
||||
```
|
||||
|
||||
## Useful commands
|
||||
|
||||
```bash
|
||||
bundle exec kamal app logs --follow
|
||||
bundle exec kamal app details
|
||||
bundle exec kamal app exec --interactive --reuse "bin/rails console"
|
||||
bundle exec kamal rollback
|
||||
```
|
||||
|
||||
## Initial app setup
|
||||
|
||||
Visit `https://your-domain/setup` once after the first deploy to create the
|
||||
admin account.
|
||||
@@ -0,0 +1,86 @@
|
||||
module Middleware
|
||||
class SanastoCors
|
||||
ALLOWED_APP_ID = ENV.fetch("SANASTO_APP_ID", "app.sanasto").freeze
|
||||
APP_ID_HEADER = "HTTP_X_SANASTO_APP"
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if allow_cors_for?(env)
|
||||
if env["REQUEST_METHOD"] == "OPTIONS"
|
||||
return preflight_response(env["HTTP_ORIGIN"], allowed_request_headers(env))
|
||||
end
|
||||
end
|
||||
|
||||
status, headers, body = @app.call(env)
|
||||
if allow_cors_for?(env)
|
||||
apply_cors_headers(headers, env["HTTP_ORIGIN"])
|
||||
end
|
||||
[ status, headers, body ]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allow_cors_for?(env)
|
||||
origin = env["HTTP_ORIGIN"].to_s
|
||||
return false if origin.empty?
|
||||
|
||||
if env["REQUEST_METHOD"] == "OPTIONS"
|
||||
return preflight_includes_app_id_header?(env)
|
||||
end
|
||||
|
||||
app_id = env[APP_ID_HEADER].to_s
|
||||
return false if app_id.empty?
|
||||
|
||||
app_id == ALLOWED_APP_ID
|
||||
end
|
||||
|
||||
def preflight_includes_app_id_header?(env)
|
||||
access_control_headers = env["HTTP_ACCESS_CONTROL_REQUEST_HEADERS"].to_s
|
||||
return false if access_control_headers.empty?
|
||||
|
||||
access_control_headers
|
||||
.split(",")
|
||||
.map { |header_name| header_name.strip.downcase }
|
||||
.include?("x-sanasto-app")
|
||||
end
|
||||
|
||||
def allowed_request_headers(env)
|
||||
access_control_headers = env["HTTP_ACCESS_CONTROL_REQUEST_HEADERS"].to_s
|
||||
return default_allowed_headers if access_control_headers.empty?
|
||||
|
||||
sanitized = access_control_headers
|
||||
.split(",")
|
||||
.map { |header_name| header_name.strip }
|
||||
.reject(&:empty?)
|
||||
.join(", ")
|
||||
|
||||
sanitized.empty? ? default_allowed_headers : sanitized
|
||||
end
|
||||
|
||||
def preflight_response(origin, allowed_headers)
|
||||
headers = {}
|
||||
apply_cors_headers(headers, origin, allowed_headers)
|
||||
headers["Access-Control-Max-Age"] = "86400"
|
||||
headers["Vary"] = [
|
||||
headers["Vary"],
|
||||
"Access-Control-Request-Headers",
|
||||
"Access-Control-Request-Method"
|
||||
].compact.join(", ")
|
||||
[ 204, headers, [] ]
|
||||
end
|
||||
|
||||
def apply_cors_headers(headers, origin, allowed_headers = default_allowed_headers)
|
||||
headers["Access-Control-Allow-Origin"] = origin
|
||||
headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||
headers["Access-Control-Allow-Headers"] = allowed_headers
|
||||
headers["Vary"] = [ headers["Vary"], "Origin, X-Sanasto-App" ].compact.join(", ")
|
||||
end
|
||||
|
||||
def default_allowed_headers
|
||||
"Origin, Content-Type, Accept, Authorization, X-Sanasto-App"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 23 KiB |
@@ -41,7 +41,8 @@ class EntriesControllerTest < ActionDispatch::IntegrationTest
|
||||
test "should paginate results" do
|
||||
get entries_url, params: { page: 2 }
|
||||
assert_response :success
|
||||
assert_select "div", text: /Page 2 of/i
|
||||
assert_select "a", text: "Previous"
|
||||
assert_select "a", text: "Next"
|
||||
end
|
||||
|
||||
test "should handle invalid language code" do
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
require "test_helper"
|
||||
|
||||
class ApiEntriesTest < ActionDispatch::IntegrationTest
|
||||
test "returns active entries with language fields" do
|
||||
entry = entries(:one)
|
||||
|
||||
get "/api/entries"
|
||||
|
||||
assert_response :success
|
||||
payload = JSON.parse(response.body)
|
||||
entry_payload = payload.find { |item| item["id"] == entry.id }
|
||||
|
||||
assert_not_nil entry_payload
|
||||
assert_equal entry.fi, entry_payload["fi"]
|
||||
assert_equal entry.en, entry_payload["en"]
|
||||
assert_equal entry.sv, entry_payload["sv"]
|
||||
assert_equal entry.no, entry_payload["no"]
|
||||
assert_equal entry.ru, entry_payload["ru"]
|
||||
assert_equal entry.de, entry_payload["de"]
|
||||
end
|
||||
|
||||
test "filters by updated_at when since param is provided" do
|
||||
older_entry = Entry.create!(
|
||||
fi: "Older Entry",
|
||||
category: :word,
|
||||
status: :active
|
||||
)
|
||||
older_entry.update_column(:updated_at, 2.days.ago)
|
||||
|
||||
newer_entry = Entry.create!(
|
||||
fi: "Newer Entry",
|
||||
category: :word,
|
||||
status: :active
|
||||
)
|
||||
newer_entry.update_column(:updated_at, 1.hour.ago)
|
||||
|
||||
get "/api/entries", params: { since: 1.day.ago.iso8601 }
|
||||
|
||||
assert_response :success
|
||||
payload = JSON.parse(response.body)
|
||||
returned_ids = payload.map { |item| item["id"] }
|
||||
|
||||
assert_includes returned_ids, newer_entry.id
|
||||
assert_not_includes returned_ids, older_entry.id
|
||||
end
|
||||
|
||||
test "returns bad request for invalid since param" do
|
||||
get "/api/entries", params: { since: "not-a-time" }
|
||||
|
||||
assert_response :bad_request
|
||||
payload = JSON.parse(response.body)
|
||||
assert_equal "Invalid since parameter. Use ISO8601 timestamp.", payload["error"]
|
||||
end
|
||||
|
||||
test "exposes swagger docs" do
|
||||
get "/api/swagger"
|
||||
|
||||
assert_response :success
|
||||
payload = JSON.parse(response.body)
|
||||
assert_equal "Sanasto Wiki API", payload.dig("info", "title")
|
||||
assert_includes payload.keys, "paths"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
require "test_helper"
|
||||
|
||||
class CorsPreflightTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@origin = "http://localhost:5173"
|
||||
@app_id = "app.sanasto"
|
||||
end
|
||||
|
||||
test "options preflight returns cors headers when app header is requested" do
|
||||
options "/api/entries", headers: {
|
||||
"Origin" => @origin,
|
||||
"Access-Control-Request-Method" => "GET",
|
||||
"Access-Control-Request-Headers" => "x-sanasto-app"
|
||||
}
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal @origin, response.headers["Access-Control-Allow-Origin"]
|
||||
assert_includes response.headers["Access-Control-Allow-Headers"], "x-sanasto-app"
|
||||
end
|
||||
|
||||
test "get includes cors headers when app id is provided" do
|
||||
get "/api/entries", headers: {
|
||||
"Origin" => @origin,
|
||||
"X-Sanasto-App" => @app_id
|
||||
}
|
||||
|
||||
assert_response :success
|
||||
assert_equal @origin, response.headers["Access-Control-Allow-Origin"]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,55 @@
|
||||
require "test_helper"
|
||||
|
||||
class SanastoCorsTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@app = ->(_env) { [ 200, {}, [ "ok" ] ] }
|
||||
@middleware = Middleware::SanastoCors.new(@app)
|
||||
@origin = "http://localhost:5173"
|
||||
@app_id = "app.sanasto"
|
||||
end
|
||||
|
||||
test "adds cors headers for allowed get requests" do
|
||||
env = Rack::MockRequest.env_for(
|
||||
"/api/entries",
|
||||
method: "GET",
|
||||
"HTTP_ORIGIN" => @origin,
|
||||
"HTTP_X_SANASTO_APP" => @app_id
|
||||
)
|
||||
|
||||
status, headers, _body = @middleware.call(env)
|
||||
|
||||
assert_equal 200, status
|
||||
assert_equal @origin, headers["Access-Control-Allow-Origin"]
|
||||
assert_includes headers["Access-Control-Allow-Headers"], "X-Sanasto-App"
|
||||
end
|
||||
|
||||
test "returns preflight response with cors headers when app header is requested" do
|
||||
env = Rack::MockRequest.env_for(
|
||||
"/api/entries",
|
||||
method: "OPTIONS",
|
||||
"HTTP_ORIGIN" => @origin,
|
||||
"HTTP_ACCESS_CONTROL_REQUEST_METHOD" => "GET",
|
||||
"HTTP_ACCESS_CONTROL_REQUEST_HEADERS" => "x-sanasto-app"
|
||||
)
|
||||
|
||||
status, headers, _body = @middleware.call(env)
|
||||
|
||||
assert_equal 204, status
|
||||
assert_equal @origin, headers["Access-Control-Allow-Origin"]
|
||||
assert_includes headers["Access-Control-Allow-Headers"], "x-sanasto-app"
|
||||
assert_includes headers["Vary"], "Access-Control-Request-Headers"
|
||||
end
|
||||
|
||||
test "does not add cors headers when app id is missing" do
|
||||
env = Rack::MockRequest.env_for(
|
||||
"/api/entries",
|
||||
method: "GET",
|
||||
"HTTP_ORIGIN" => @origin
|
||||
)
|
||||
|
||||
status, headers, _body = @middleware.call(env)
|
||||
|
||||
assert_equal 200, status
|
||||
assert_nil headers["Access-Control-Allow-Origin"]
|
||||
end
|
||||
end
|
||||
@@ -102,7 +102,7 @@ class PublicBrowsingTest < ApplicationSystemTestCase
|
||||
visit root_path
|
||||
|
||||
assert_selector ".entry-row", count: 25
|
||||
assert_link "2"
|
||||
assert_link "Next"
|
||||
end
|
||||
|
||||
test "visitor sees entry statistics" do
|
||||
|
||||