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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/app/assets/config/active_hashcash_manifest.js +1 -0
- data/app/assets/javascripts/hashcash.js +257 -0
- data/app/assets/stylesheets/active_hashcash/application.css +15 -0
- data/app/controllers/active_hashcash/addresses_controller.rb +7 -0
- data/app/controllers/active_hashcash/application_controller.rb +4 -0
- data/app/controllers/active_hashcash/assets_controller.rb +34 -0
- data/app/controllers/active_hashcash/stamps_controller.rb +11 -0
- data/app/helpers/active_hashcash/addresses_helper.rb +4 -0
- data/app/helpers/active_hashcash/application_helper.rb +4 -0
- data/app/helpers/active_hashcash/stamps_helper.rb +4 -0
- data/app/jobs/active_hashcash/application_job.rb +4 -0
- data/app/mailers/active_hashcash/application_mailer.rb +6 -0
- data/app/models/active_hashcash/application_record.rb +5 -0
- data/app/models/active_hashcash/stamp.rb +70 -0
- data/app/views/active_hashcash/addresses/index.html.erb +17 -0
- data/app/views/active_hashcash/assets/_logo.svg.erb +1 -0
- data/app/views/active_hashcash/assets/_style.css +148 -0
- data/app/views/active_hashcash/assets/application.css.erb +1 -0
- data/app/views/active_hashcash/assets/ariato.css.erb +2 -0
- data/app/views/active_hashcash/assets/favicon.ico +0 -0
- data/app/views/active_hashcash/assets/favicon.svg.erb +1 -0
- data/app/views/active_hashcash/assets/vendor/_ariato_base.css +1297 -0
- data/app/views/active_hashcash/assets/vendor/_ariato_extra.css +1206 -0
- data/app/views/active_hashcash/stamps/_filters.html.erb +39 -0
- data/app/views/active_hashcash/stamps/index.html.erb +25 -0
- data/app/views/active_hashcash/stamps/show.html.erb +21 -0
- data/app/views/layouts/active_hashcash/application.html.erb +36 -0
- data/config/locales/de.yml +4 -0
- data/config/locales/en.yml +4 -0
- data/config/locales/es.yml +4 -0
- data/config/locales/fr.yml +4 -0
- data/config/locales/it.yml +4 -0
- data/config/locales/jp.yml +4 -0
- data/config/locales/pt.yml +4 -0
- data/config/routes.rb +6 -0
- data/db/migrate/20240215143453_create_active_hashcash_stamps.rb +25 -0
- data/lib/active_hashcash/version.rb +1 -1
- metadata +39 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dbc710b7b2cc6fef0667915ac1a95ccfdd6d1cb3eaa1fa9b97d29ef81e79985d
|
4
|
+
data.tar.gz: 323c22e60aa27d2d87e39ee8f5c356d2fc6e8190426e49831a29d762d2e74f28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed74328b7b1f91eb2dece7a29efb5b9d68079872694e589caca3e79de1a06e6d0bd5d2556dcab12f92fcede4c08b075a4dc4b9077e79c8511cb4045ce509c82c
|
7
|
+
data.tar.gz: e8f580f0668529b3bd7e220f2bdbd4b2eae6bc63272228b9cb4d1fe75ef0e30202983e3e06d91664558d36239137b69d7ba96996c80b370f4b1638300f77781a
|
data/CHANGELOG.md
CHANGED
@@ -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,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,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" %>
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render "/active_hashcash/assets/logo" %>
|