active_hashcash 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/app/assets/config/active_hashcash_manifest.js +1 -0
  4. data/app/assets/javascripts/hashcash.js +257 -0
  5. data/app/assets/stylesheets/active_hashcash/application.css +15 -0
  6. data/app/controllers/active_hashcash/addresses_controller.rb +7 -0
  7. data/app/controllers/active_hashcash/application_controller.rb +4 -0
  8. data/app/controllers/active_hashcash/assets_controller.rb +34 -0
  9. data/app/controllers/active_hashcash/stamps_controller.rb +11 -0
  10. data/app/helpers/active_hashcash/addresses_helper.rb +4 -0
  11. data/app/helpers/active_hashcash/application_helper.rb +4 -0
  12. data/app/helpers/active_hashcash/stamps_helper.rb +4 -0
  13. data/app/jobs/active_hashcash/application_job.rb +4 -0
  14. data/app/mailers/active_hashcash/application_mailer.rb +6 -0
  15. data/app/models/active_hashcash/application_record.rb +5 -0
  16. data/app/models/active_hashcash/stamp.rb +70 -0
  17. data/app/views/active_hashcash/addresses/index.html.erb +17 -0
  18. data/app/views/active_hashcash/assets/_logo.svg.erb +1 -0
  19. data/app/views/active_hashcash/assets/_style.css +148 -0
  20. data/app/views/active_hashcash/assets/application.css.erb +1 -0
  21. data/app/views/active_hashcash/assets/ariato.css.erb +2 -0
  22. data/app/views/active_hashcash/assets/favicon.ico +0 -0
  23. data/app/views/active_hashcash/assets/favicon.svg.erb +1 -0
  24. data/app/views/active_hashcash/assets/vendor/_ariato_base.css +1297 -0
  25. data/app/views/active_hashcash/assets/vendor/_ariato_extra.css +1206 -0
  26. data/app/views/active_hashcash/stamps/_filters.html.erb +39 -0
  27. data/app/views/active_hashcash/stamps/index.html.erb +25 -0
  28. data/app/views/active_hashcash/stamps/show.html.erb +21 -0
  29. data/app/views/layouts/active_hashcash/application.html.erb +36 -0
  30. data/config/locales/de.yml +4 -0
  31. data/config/locales/en.yml +4 -0
  32. data/config/locales/es.yml +4 -0
  33. data/config/locales/fr.yml +4 -0
  34. data/config/locales/it.yml +4 -0
  35. data/config/locales/jp.yml +4 -0
  36. data/config/locales/pt.yml +4 -0
  37. data/config/routes.rb +6 -0
  38. data/db/migrate/20240215143453_create_active_hashcash_stamps.rb +25 -0
  39. data/lib/active_hashcash/version.rb +1 -1
  40. metadata +39 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29b1886eff044771c56c4eeff7dfcc4a305262fd39b9633a43522e1a8a465f7a
4
- data.tar.gz: 0ad53ba4ebdcec34064f33cc1ea2f75dc95ce6ffb57a733604174e0059c37521
3
+ metadata.gz: dbc710b7b2cc6fef0667915ac1a95ccfdd6d1cb3eaa1fa9b97d29ef81e79985d
4
+ data.tar.gz: 323c22e60aa27d2d87e39ee8f5c356d2fc6e8190426e49831a29d762d2e74f28
5
5
  SHA512:
6
- metadata.gz: 794687bc50403b03a63e4855451bec53751b6da4a20ef1674bdbf0d66dfdfacfaa68f3d1416a14a0a1bdf823b4641b8a16e6b70aba288278883170c7a73b6202
7
- data.tar.gz: 7b0ca6f84cb75b2e49b5747034393c778023a2efb6974b97ba9fab084074da57cb78f5014de5d26e20af378bb8a86788f833f02e036e5feb56e8aee52c3b87d9
6
+ metadata.gz: ed74328b7b1f91eb2dece7a29efb5b9d68079872694e589caca3e79de1a06e6d0bd5d2556dcab12f92fcede4c08b075a4dc4b9077e79c8511cb4045ce509c82c
7
+ data.tar.gz: e8f580f0668529b3bd7e220f2bdbd4b2eae6bc63272228b9cb4d1fe75ef0e30202983e3e06d91664558d36239137b69d7ba96996c80b370f4b1638300f77781a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog of ActiveHashcash
2
2
 
3
+ ## Unrelease
4
+
5
+ - Fix gem spec list files
6
+
3
7
  ## 0.3.0 - 2024-03-14
4
8
 
5
9
  - Increase complexity automatically to slowdown brute force attacks
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/active_hashcash .css
@@ -0,0 +1,257 @@
1
+ // http://www.hashcash.org/docs/hashcash.html
2
+ // <input type="hiden" name="hashcash" data-hashcash="{resource: 'site.example', bits: 16}"/>
3
+ Hashcash = function(input) {
4
+ options = JSON.parse(input.getAttribute("data-hashcash"))
5
+ Hashcash.disableParentForm(input, options)
6
+ input.dispatchEvent(new CustomEvent("hashcash:mint", {bubbles: true}))
7
+
8
+ Hashcash.mint(options.resource, options, function(stamp) {
9
+ input.value = stamp.toString()
10
+ Hashcash.enableParentForm(input, options)
11
+ input.dispatchEvent(new CustomEvent("hashcash:minted", {bubbles: true, detail: {stamp: stamp}}))
12
+ })
13
+ }
14
+
15
+ Hashcash.setup = function() {
16
+ if (document.readyState != "loading") {
17
+ var input = document.querySelector("input#hashcash")
18
+ input && new Hashcash(input)
19
+ } else
20
+ document.addEventListener("DOMContentLoaded", Hashcash.setup )
21
+ }
22
+
23
+ Hashcash.disableParentForm = function(input, options) {
24
+ input.form.querySelectorAll("[type=submit]").forEach(function(submit) {
25
+ submit.originalValue = submit.value
26
+ options["waiting_message"] && (submit.value = options["waiting_message"])
27
+ submit.disabled = true
28
+ })
29
+ }
30
+
31
+ Hashcash.enableParentForm = function(input, options) {
32
+ input.form.querySelectorAll("[type=submit]").forEach(function(submit) {
33
+ submit.originalValue && (submit.value = submit.originalValue)
34
+ submit.disabled = null
35
+ })
36
+ }
37
+
38
+ Hashcash.default = {
39
+ version: 1,
40
+ bits: 20,
41
+ extension: null,
42
+ }
43
+
44
+ Hashcash.mint = function(resource, options, callback) {
45
+ // Format date to YYMMDD
46
+ var date = new Date
47
+ var year = date.getFullYear().toString()
48
+ year = year.slice(year.length - 2, year.length)
49
+ var month = (date.getMonth() + 1).toString().padStart(2, "0")
50
+ var day = date.getDate().toString().padStart(2, "0")
51
+
52
+ var stamp = new Hashcash.Stamp(
53
+ options.version || Hashcash.default.version,
54
+ options.bits || Hashcash.default.bits,
55
+ options.date || year + month + day,
56
+ resource,
57
+ options.extension || Hashcash.default.extension,
58
+ options.rand || Math.random().toString(36).substr(2, 10),
59
+ )
60
+ return stamp.work(callback)
61
+ }
62
+
63
+ Hashcash.Stamp = function(version, bits, date, resource, extension, rand, counter = 0) {
64
+ this.version = version
65
+ this.bits = bits
66
+ this.date = date
67
+ this.resource = resource
68
+ this.extension = extension
69
+ this.rand = rand
70
+ this.counter = counter
71
+ }
72
+
73
+ Hashcash.Stamp.parse = function(string) {
74
+ var args = string.split(":")
75
+ return new Hashcash.Stamp(args[0], args[1], args[2], args[3], args[4], args[5], args[6])
76
+ }
77
+
78
+ Hashcash.Stamp.prototype.toString = function() {
79
+ return [this.version, this.bits, this.date, this.resource, this.extension, this.rand, this.counter].join(":")
80
+ }
81
+
82
+ // Trigger the given callback when the problem is solved.
83
+ // In order to not freeze the page, setTimeout is called every 100ms to let some CPU to other tasks.
84
+ Hashcash.Stamp.prototype.work = function(callback) {
85
+ this.startClock()
86
+ var timer = performance.now()
87
+ while (!this.check())
88
+ if (this.counter++ && performance.now() - timer > 100)
89
+ return setTimeout(this.work.bind(this), 0, callback)
90
+ this.stopClock()
91
+ callback(this)
92
+ }
93
+
94
+ Hashcash.Stamp.prototype.check = function() {
95
+ var array = Hashcash.sha1(this.toString())
96
+ return array[0] >> (160-this.bits) == 0
97
+ }
98
+
99
+ Hashcash.Stamp.prototype.startClock = function() {
100
+ this.startedAt || (this.startedAt = performance.now())
101
+ }
102
+
103
+ Hashcash.Stamp.prototype.stopClock = function() {
104
+ this.endedAt || (this.endedAt = performance.now())
105
+ var duration = this.endedAt - this.startedAt
106
+ var speed = Math.round(this.counter * 1000 / duration)
107
+ console.debug("Hashcash " + this.toString() + " minted in " + duration + "ms (" + speed + " per seconds)")
108
+ }
109
+
110
+ /**
111
+ * Secure Hash Algorithm (SHA1)
112
+ * http://www.webtoolkit.info/
113
+ **/
114
+ Hashcash.sha1 = function(msg) {
115
+ var rotate_left = Hashcash.sha1.rotate_left
116
+ var Utf8Encode = Hashcash.sha1.Utf8Encode
117
+
118
+ var blockstart;
119
+ var i, j;
120
+ var W = new Array(80);
121
+ var H0 = 0x67452301;
122
+ var H1 = 0xEFCDAB89;
123
+ var H2 = 0x98BADCFE;
124
+ var H3 = 0x10325476;
125
+ var H4 = 0xC3D2E1F0;
126
+ var A, B, C, D, E;
127
+ var temp;
128
+ msg = Utf8Encode(msg);
129
+ var msg_len = msg.length;
130
+ var word_array = new Array();
131
+ for (i = 0; i < msg_len - 3; i += 4) {
132
+ j = msg.charCodeAt(i) << 24 | msg.charCodeAt(i + 1) << 16 |
133
+ msg.charCodeAt(i + 2) << 8 | msg.charCodeAt(i + 3);
134
+ word_array.push(j);
135
+ }
136
+ switch (msg_len % 4) {
137
+ case 0:
138
+ i = 0x080000000;
139
+ break;
140
+ case 1:
141
+ i = msg.charCodeAt(msg_len - 1) << 24 | 0x0800000;
142
+ break;
143
+ case 2:
144
+ i = msg.charCodeAt(msg_len - 2) << 24 | msg.charCodeAt(msg_len - 1) << 16 | 0x08000;
145
+ break;
146
+ case 3:
147
+ i = msg.charCodeAt(msg_len - 3) << 24 | msg.charCodeAt(msg_len - 2) << 16 | msg.charCodeAt(msg_len - 1) << 8 | 0x80;
148
+ break;
149
+ }
150
+ word_array.push(i);
151
+ while ((word_array.length % 16) != 14) word_array.push(0);
152
+ word_array.push(msg_len >>> 29);
153
+ word_array.push((msg_len << 3) & 0x0ffffffff);
154
+ for (blockstart = 0; blockstart < word_array.length; blockstart += 16) {
155
+ for (i = 0; i < 16; i++) W[i] = word_array[blockstart + i];
156
+ for (i = 16; i <= 79; i++) W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1);
157
+ A = H0;
158
+ B = H1;
159
+ C = H2;
160
+ D = H3;
161
+ E = H4;
162
+ for (i = 0; i <= 19; i++) {
163
+ temp = (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff;
164
+ E = D;
165
+ D = C;
166
+ C = rotate_left(B, 30);
167
+ B = A;
168
+ A = temp;
169
+ }
170
+ for (i = 20; i <= 39; i++) {
171
+ temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff;
172
+ E = D;
173
+ D = C;
174
+ C = rotate_left(B, 30);
175
+ B = A;
176
+ A = temp;
177
+ }
178
+ for (i = 40; i <= 59; i++) {
179
+ temp = (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff;
180
+ E = D;
181
+ D = C;
182
+ C = rotate_left(B, 30);
183
+ B = A;
184
+ A = temp;
185
+ }
186
+ for (i = 60; i <= 79; i++) {
187
+ temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff;
188
+ E = D;
189
+ D = C;
190
+ C = rotate_left(B, 30);
191
+ B = A;
192
+ A = temp;
193
+ }
194
+ H0 = (H0 + A) & 0x0ffffffff;
195
+ H1 = (H1 + B) & 0x0ffffffff;
196
+ H2 = (H2 + C) & 0x0ffffffff;
197
+ H3 = (H3 + D) & 0x0ffffffff;
198
+ H4 = (H4 + E) & 0x0ffffffff;
199
+ }
200
+ return [H0, H1, H2, H3, H4]
201
+ }
202
+
203
+ Hashcash.hexSha1 = function(msg) {
204
+ var array = Hashcash.sha1(msg)
205
+ var cvt_hex = Hashcash.sha1.cvt_hex
206
+ return cvt_hex(array[0]) + cvt_hex(array[1]) + cvt_hex(array[2]) + cvt_hex(array3) + cvt_hex(array[4])
207
+ }
208
+
209
+ Hashcash.sha1.rotate_left = function(n, s) {
210
+ var t4 = (n << s) | (n >>> (32 - s));
211
+ return t4;
212
+ };
213
+
214
+ Hashcash.sha1.lsb_hex = function(val) {
215
+ var str = '';
216
+ var i;
217
+ var vh;
218
+ var vl;
219
+ for (i = 0; i <= 6; i += 2) {
220
+ vh = (val >>> (i * 4 + 4)) & 0x0f;
221
+ vl = (val >>> (i * 4)) & 0x0f;
222
+ str += vh.toString(16) + vl.toString(16);
223
+ }
224
+ return str;
225
+ };
226
+
227
+ Hashcash.sha1.cvt_hex = function(val) {
228
+ var str = '';
229
+ var i;
230
+ var v;
231
+ for (i = 7; i >= 0; i--) {
232
+ v = (val >>> (i * 4)) & 0x0f;
233
+ str += v.toString(16);
234
+ }
235
+ return str;
236
+ };
237
+
238
+ Hashcash.sha1.Utf8Encode = function(string) {
239
+ string = string.replace(/\r\n/g, '\n');
240
+ var utftext = '';
241
+ for (var n = 0; n < string.length; n++) {
242
+ var c = string.charCodeAt(n);
243
+ if (c < 128) {
244
+ utftext += String.fromCharCode(c);
245
+ } else if ((c > 127) && (c < 2048)) {
246
+ utftext += String.fromCharCode((c >> 6) | 192);
247
+ utftext += String.fromCharCode((c & 63) | 128);
248
+ } else {
249
+ utftext += String.fromCharCode((c >> 12) | 224);
250
+ utftext += String.fromCharCode(((c >> 6) & 63) | 128);
251
+ utftext += String.fromCharCode((c & 63) | 128);
252
+ }
253
+ }
254
+ return utftext;
255
+ };
256
+
257
+ Hashcash.setup()
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,7 @@
1
+ module ActiveHashcash
2
+ class AddressesController < ApplicationController
3
+ def index
4
+ @addresses = Stamp.filter_by(params).group(:ip_address).order(count_all: :desc).limit(1000).count
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveHashcash
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,34 @@
1
+ module ActiveHashcash
2
+ class AssetsController < ApplicationController
3
+ protect_from_forgery except: :show
4
+
5
+ Mime::Type.register "image/x-icon", :ico
6
+
7
+ def show
8
+ if endpoints.include?(file_name = File.basename(request.path))
9
+ file_path = ActiveHashcash::Engine.root.join / "app/views/active_hashcash/assets" / file_name
10
+ if File.exists?("#{file_path}.erb")
11
+ render(params[:id], mime_type: mime_type)
12
+ else
13
+ render(file: file_path)
14
+ end
15
+ expires_in(1.day, public: true)
16
+ else
17
+ raise ActionController::RoutingError.new
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def endpoints
24
+ return @endpoints if @endpoints
25
+ folder = ActiveHashcash::Engine.root.join("app/views", controller_path)
26
+ files = folder.each_child.map { |path| File.basename(path).delete_suffix(".erb") }
27
+ @endpoints = files.delete_if { |str| str.start_with?("_") }
28
+ end
29
+
30
+ def mime_type
31
+ Mime::Type.lookup_by_extension(File.extname(request.path).delete_prefix("."))
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveHashcash
2
+ class StampsController < ApplicationController
3
+ def index
4
+ @stamps = Stamp.filter_by(params).order(created_at: :desc).limit(1000)
5
+ end
6
+
7
+ def show
8
+ @stamp = Stamp.find(params[:id])
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveHashcash
2
+ module AddressesHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveHashcash
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveHashcash
2
+ module StampsHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveHashcash
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module ActiveHashcash
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveHashcash
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveHashcash
4
+ class Stamp < ApplicationRecord
5
+ validates_presence_of :version, :bits, :date, :resource, :rand, :counter
6
+
7
+ scope :created_from, -> (date) { where(created_at: date..) }
8
+ scope :created_to, -> (date) { where(created_at: ..date) }
9
+
10
+ scope :bits_from, -> (value) { where(bits: value..) }
11
+ scope :bits_to, -> (value) { where(bits: ..value) }
12
+
13
+ scope :ip_address_starts_with, -> (string) { where("ip_address LIKE ?", sanitize_sql_like(string) + "%") }
14
+ scope :request_path_starts_with, -> (string) { where("request_path LIKE ?", sanitize_sql_like(string) + "%") }
15
+
16
+ def self.filter_by(params)
17
+ scope = all
18
+ scope = scope.request_path_starts_with(params[:request_path_starts_with]) if params[:request_path_starts_with].present?
19
+ scope = scope.ip_address_starts_with(params[:ip_address_starts_with]) if params[:ip_address_starts_with].present?
20
+ scope = scope.created_from(params[:created_from]) if params[:created_from].present?
21
+ scope = scope.created_to(params[:created_to]) if params[:created_to].present?
22
+ scope = scope.bits_from(params[:bits_from]) if params[:bits_from].present?
23
+ scope = scope.bits_to(params[:bits_to]) if params[:bits_to].present?
24
+ scope
25
+ end
26
+
27
+ def self.spend(string, resource, bits, date, options = {})
28
+ return false unless stamp = parse(string)
29
+ stamp.attributes = options
30
+ stamp.verify(resource, bits, date) && stamp.save
31
+ rescue ActiveRecord::RecordNotUnique
32
+ false
33
+ end
34
+
35
+ def self.parse(string)
36
+ return unless string
37
+ args = string.split(":")
38
+ return if args.size != 7
39
+ new(version: args[0], bits: args[1], date: Date.strptime(args[2], ActiveHashcash.date_format), resource: args[3], ext: args[4], rand: args[5], counter: args[6])
40
+ end
41
+
42
+ def self.mint(resource, attributes = {})
43
+ new({
44
+ version: 1,
45
+ bits: ActiveHashcash.bits,
46
+ date: Date.today.strftime(ActiveHashcash.date_format),
47
+ resource: resource,
48
+ rand: SecureRandom.alphanumeric(16),
49
+ counter: 0,
50
+ }.merge(attributes)).work
51
+ end
52
+
53
+ def work
54
+ counter.next! until authentic?
55
+ self
56
+ end
57
+
58
+ def authentic?
59
+ Digest::SHA1.hexdigest(to_s).hex >> (160-bits) == 0
60
+ end
61
+
62
+ def verify(resource, bits, date)
63
+ self.resource == resource && self.bits >= bits && self.date >= date && !self.date.future? && authentic?
64
+ end
65
+
66
+ def to_s
67
+ [version.to_i, bits, date.strftime("%y%m%d"), resource, ext, rand, counter].join(":")
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,17 @@
1
+ <details>
2
+ <summary>Filters</summary>
3
+ <%= render "active_hashcash/stamps/filters" %>
4
+ </details>
5
+
6
+ <table>
7
+ <tr>
8
+ <th><%= ActiveHashcash::Stamp.human_attribute_name(:ip_address) %></th>
9
+ <th><%= ActiveHashcash::Stamp.model_name.human.pluralize %></th>
10
+ </tr>
11
+ <% for (address, count) in @addresses %>
12
+ <tr>
13
+ <td><%= link_to address, stamps_path(ip_address_starts_with: address) %></td>
14
+ <td><%= number_with_delimiter count %></td>
15
+ </tr>
16
+ <% end %>
17
+ </table>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 50.4 50.4" xmlns="http://www.w3.org/2000/svg"><g fill="#08773c"><path d="m25.2 0c-13.9 0-25.2 11.3-25.2 25.2s11.3 25.2 25.2 25.2 25.2-11.3 25.2-25.2-11.31-25.2-25.2-25.2zm0 46.4c-11.69 0-21.2-9.51-21.2-21.2s9.51-21.2 21.2-21.2 21.2 9.51 21.2 21.2-9.51 21.2-21.2 21.2z"/><path d="m33 12.04h-4v7.58h-7.89v-7.58h-4v7.58h-4.98v4h4.98v2.69h-4.98v4h4.98v7.58h4v-7.58h7.89v7.58h4v-7.58h4.98v-4h-4.98v-2.69h4.98v-4h-4.98zm-4 14.27h-7.89v-2.69h7.89z"/></g></svg>
@@ -0,0 +1,148 @@
1
+ body {
2
+ padding: 0;
3
+ margin: 0;
4
+ width: 100%;
5
+ }
6
+
7
+ body > header nav {
8
+ width: 100%;
9
+ max-width: 1280px;
10
+ margin: 0 auto;
11
+ padding: 24px;
12
+ display: flex;
13
+ flex-wrap: wrap;
14
+ align-items: center;
15
+ min-height: 88px;
16
+ }
17
+
18
+ header nav .menubutton {
19
+ margin-left: auto;
20
+ }
21
+
22
+ header .logo {
23
+ margin-right: 8px;
24
+ }
25
+
26
+ header .logo svg {
27
+ width: 24px;
28
+ height: 24px;
29
+ fill: none;
30
+ stroke: rgba(var(--color-blue-500), 1);
31
+ stroke-width: 1;
32
+ vertical-align: middle;
33
+ }
34
+
35
+ header .is-link {
36
+ text-transform: none;
37
+ text-decoration: none;
38
+ font-size: 1rem;
39
+ letter-spacing: 0;
40
+ font-weight: 400;
41
+ padding: 0 16px;
42
+ margin: 0;
43
+ min-height: 24px;
44
+ color: rgba(var(--color-grey-700), 1);
45
+ }
46
+
47
+ header .is-link:hover {
48
+ background: rgba(var(--color-grey-50), 1);
49
+ }
50
+
51
+ main {
52
+ width: 100%;
53
+ max-width: 1280px;
54
+ margin: 0 auto;
55
+ padding: 0 24px;
56
+ }
57
+
58
+ main h2 {
59
+ word-break: break-word;
60
+ display: flex;
61
+ gap: var(--space);
62
+ }
63
+
64
+ section {
65
+ margin-bottom: var(--space-4x);
66
+ }
67
+
68
+ .tooltip-date {
69
+ color: rgba(var(--color-grey-100), 1);
70
+ }
71
+
72
+ .card h3 {
73
+ display: flex;
74
+ }
75
+
76
+ .card h3 small {
77
+ color: rgba(var(--color-red-500), 1);
78
+ }
79
+
80
+ .card h3 small.is-success {
81
+ color: rgba(var(--color-green-500), 1);
82
+ }
83
+
84
+ .card h3 a {
85
+ margin-left: auto;
86
+ font-weight: 400;
87
+ font-size: 1rem;
88
+ }
89
+
90
+ .card table {
91
+ margin: 0 -24px -24px;
92
+ width: calc(100% + 48px);
93
+ }
94
+
95
+ td a {
96
+ word-break: break-word;
97
+ }
98
+
99
+ td.number {
100
+ text-align: right;
101
+ font-variant-numeric: tabular-nums;
102
+ }
103
+
104
+ div.is-empty {
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ height: calc(100% - 72px);
109
+ width: 100%;
110
+ color: rgba(var(--color-grey-300), 1);
111
+ }
112
+
113
+ body > footer {
114
+ max-width: 1280px;
115
+ margin: 48px auto 0;
116
+ padding: 24px;
117
+ }
118
+
119
+ body > footer ul {
120
+ margin: 0;
121
+ list-style-type: none;
122
+ padding: 0;
123
+ }
124
+
125
+ body > footer .card {
126
+ margin: 0;
127
+ background: rgba(var(--color-grey-50), 1);
128
+ }
129
+
130
+ body > footer .card p {
131
+ padding: 0 0 2px;
132
+ color: rgba(var(--color-grey-400), 1);
133
+ }
134
+
135
+ @media (max-width: 480px) {
136
+ header nav .menubutton {
137
+ margin-top: 24px;
138
+ }
139
+
140
+ [role="dialog"] {
141
+ min-width: 280px;
142
+ }
143
+ }
144
+
145
+ details form {
146
+ padding: var(--space-3x) ;
147
+ padding-top: 0;
148
+ }
@@ -0,0 +1 @@
1
+ <%= render "style" %>
@@ -0,0 +1,2 @@
1
+ <%= render partial: "/active_hashcash/assets/vendor/ariato_base", formats: [:css] %>
2
+ <%= render partial: "/active_hashcash/assets/vendor/ariato_extra", formats: [:css] %>
@@ -0,0 +1 @@
1
+ <%= render "/active_hashcash/assets/logo" %>