leanweb 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/leanweb +9 -9
- data/contrib/lib/hawese.rb +62 -0
- data/contrib/lib/lean_mail.rb +125 -0
- data/contrib/lib/leanweb/controller_mixins/render_with_layout.rb +170 -0
- data/contrib/lib/rack/session/json_file.rb +84 -0
- data/lib/leanweb/app.rb +4 -4
- data/lib/leanweb/controller.rb +5 -4
- data/lib/leanweb/helpers.rb +26 -16
- data/lib/leanweb/route.rb +56 -38
- data/lib/leanweb/version.rb +13 -0
- data/lib/leanweb.rb +7 -9
- metadata +54 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d4e80c5813858b838b39fea56bb96b2fb28f7749d22f02ab04694662a8595d96
|
4
|
+
data.tar.gz: c2fdc8ebf92d647516db11352e5a6cb7bc36f1489dfabe69c095e7090f1b7970
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1bed8458a93a2f0139609f17fe3402f69065182607b6b6664cc0c6d90ab3815f4dca7cd08c165dab17e5448d9c3f7d3c44ef8e1f28468cd3e4c42a66b096c8b8
|
7
|
+
data.tar.gz: 320c619aedd70c9bd0ed5e698ef456e293d3cf754bbe3ca939a7669b45ad117a371089db15737edf0c5c271ad638b84784e6ee3a730093a82a16843828a1074c
|
data/bin/leanweb
CHANGED
@@ -3,10 +3,10 @@
|
|
3
3
|
|
4
4
|
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
5
5
|
#
|
6
|
-
# This file is part of "LeanWeb" and licensed under the terms of the
|
7
|
-
# General Public License version
|
8
|
-
# should have received a copy of this
|
9
|
-
# see <https://
|
6
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
7
|
+
# General Public License version 3 with permissions for compatibility with the
|
8
|
+
# Hacktivista General Public License. You should have received a copy of this
|
9
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
10
10
|
|
11
11
|
require 'fileutils'
|
12
12
|
require 'leanweb'
|
@@ -16,15 +16,15 @@ require 'leanweb'
|
|
16
16
|
files = [
|
17
17
|
{
|
18
18
|
filename: 'Gemfile',
|
19
|
-
# TODO: Use {LeanWeb::VERSION} when v1 is reached.
|
20
19
|
content: <<~RUBY
|
21
20
|
# frozen_string_literal: true
|
22
21
|
|
23
22
|
source 'https://rubygems.org'
|
24
23
|
|
25
|
-
gem 'leanweb', '~>
|
26
|
-
gem 'haml'
|
24
|
+
gem 'leanweb', '~> #{LeanWeb::VERSION}'
|
25
|
+
gem 'haml', '~> 5.2.2'
|
27
26
|
gem 'rake'
|
27
|
+
gem 'puma'
|
28
28
|
RUBY
|
29
29
|
}, {
|
30
30
|
filename: 'config.ru',
|
@@ -55,7 +55,7 @@ files = [
|
|
55
55
|
|
56
56
|
require 'leanweb'
|
57
57
|
|
58
|
-
task default: %w[
|
58
|
+
task default: %w[build_static]
|
59
59
|
|
60
60
|
task :build_static do
|
61
61
|
require_relative 'routes'
|
@@ -63,7 +63,7 @@ files = [
|
|
63
63
|
end
|
64
64
|
RUBY
|
65
65
|
}, {
|
66
|
-
filename: 'src/controllers/
|
66
|
+
filename: 'src/controllers/main_controller.rb',
|
67
67
|
content: <<~RUBY
|
68
68
|
# frozen_string_literal: true
|
69
69
|
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
|
+
#
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
|
+
|
10
|
+
require 'json'
|
11
|
+
require 'net/http'
|
12
|
+
require 'openssl'
|
13
|
+
require 'tilt'
|
14
|
+
require 'uri'
|
15
|
+
|
16
|
+
# Hawese client.
|
17
|
+
#
|
18
|
+
# **Environment variables**
|
19
|
+
#
|
20
|
+
# - HAWESE_RETURN_URL defaults to `LEANWEB_ENDPOINT/checkout`.
|
21
|
+
# - HAWESE_ENDPOINT
|
22
|
+
# - HAWESE_ORIGIN
|
23
|
+
class Hawese
|
24
|
+
class << self
|
25
|
+
ENDPOINT = ENV.fetch('HAWESE_ENDPOINT')
|
26
|
+
RETURN_URL = ENV.fetch('HAWESE_RETURN_URL') do
|
27
|
+
"#{ENV.fetch('LEANWEB_ENDPOINT')}/checkout"
|
28
|
+
end
|
29
|
+
ORIGIN = ENV.fetch('HAWESE_ORIGIN')
|
30
|
+
|
31
|
+
def payment_methods
|
32
|
+
uri = URI("#{ENDPOINT}/gateways/payment-methods/purchase")
|
33
|
+
JSON.parse(Net::HTTP.get(uri))
|
34
|
+
end
|
35
|
+
|
36
|
+
def gateway_schema(gateway, query_params, schema = 'purchase')
|
37
|
+
uri = URI("#{ENDPOINT}/gateways/#{gateway}/schemas/#{schema}")
|
38
|
+
uri.query = URI.encode_www_form(query_params)
|
39
|
+
JSON.parse(Net::HTTP.get(uri))
|
40
|
+
end
|
41
|
+
|
42
|
+
def purchase(gateway, fields)
|
43
|
+
fields[:origin] = ORIGIN
|
44
|
+
uri = URI("#{ENDPOINT}/gateways/#{gateway}/purchase")
|
45
|
+
uri.query = URI.encode_www_form(return_url: RETURN_URL)
|
46
|
+
response = Net::HTTP.post(
|
47
|
+
uri,
|
48
|
+
fields.to_json,
|
49
|
+
'Content-Type' => 'application/json'
|
50
|
+
)
|
51
|
+
JSON.parse(response.body)
|
52
|
+
end
|
53
|
+
|
54
|
+
def purchase_from_schema(gateway, schema)
|
55
|
+
fields = {}
|
56
|
+
schema['properties'].each do |property, contents|
|
57
|
+
fields[property] = contents['default'] if contents.include?('default')
|
58
|
+
end
|
59
|
+
purchase(gateway, fields)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
|
+
#
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
|
+
|
10
|
+
require 'net/smtp'
|
11
|
+
require 'securerandom'
|
12
|
+
require 'socket'
|
13
|
+
require 'time'
|
14
|
+
|
15
|
+
# Send an email with SMTP easily.
|
16
|
+
#
|
17
|
+
# Environment variables:
|
18
|
+
#
|
19
|
+
# - SMTP_HOST: Where to connect to.
|
20
|
+
# - SMTP_PORT: Port, optional (default: 25).
|
21
|
+
# - SMTP_SECURITY: `tls` or `starttls`, optional (default: `nil`).
|
22
|
+
# - SMTP_USER: User, optional.
|
23
|
+
# - SMTP_PASSWORD: Password, optional.
|
24
|
+
# - SMTP_FROM: In the format `Name <user@mail>` or `user@mail`.
|
25
|
+
#
|
26
|
+
# @example Single address
|
27
|
+
# LeanMail.deliver('to@mail', 'subject', 'body')
|
28
|
+
#
|
29
|
+
# @example Multiple addresses
|
30
|
+
# LeanMail.deliver(['to@mail', 'to2@mail'], 'subject', 'body')
|
31
|
+
module LeanMail
|
32
|
+
# RFC 2821 message data representation.
|
33
|
+
class Data
|
34
|
+
attr_reader :from, :to, :subject, :message
|
35
|
+
|
36
|
+
def initialize(from, to, subject, message)
|
37
|
+
@from = from
|
38
|
+
@to = to
|
39
|
+
@subject = subject
|
40
|
+
@message = message
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
<<~MAIL
|
45
|
+
From: #{@from}
|
46
|
+
To: #{to.instance_of?(Array) ? to.join(', ') : to}
|
47
|
+
Subject: #{subject}
|
48
|
+
Date: #{Time.new.rfc2822}
|
49
|
+
Message-Id: <#{SecureRandom.uuid}@#{Socket.gethostname}>
|
50
|
+
|
51
|
+
#{@message}
|
52
|
+
MAIL
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Email deliverer.
|
57
|
+
class Deliver
|
58
|
+
attr_reader :data
|
59
|
+
|
60
|
+
def initialize
|
61
|
+
@user = ENV.fetch('SMTP_USER', nil)
|
62
|
+
@password = ENV.fetch('SMTP_PASSWORD', nil)
|
63
|
+
@from = ENV.fetch('SMTP_FROM')
|
64
|
+
|
65
|
+
@host = ENV.fetch('SMTP_HOST')
|
66
|
+
@port = ENV.fetch('SMTP_PORT', 25)
|
67
|
+
@security = ENV.fetch('SMTP_SECURITY', nil)
|
68
|
+
|
69
|
+
initialize_smtp
|
70
|
+
end
|
71
|
+
|
72
|
+
# Send email.
|
73
|
+
#
|
74
|
+
# @param to [Array, String] In the format `Name <user@mail>` or `user@mail`.
|
75
|
+
# @param subject [String]
|
76
|
+
# @param body [String]
|
77
|
+
# @return [Deliver]
|
78
|
+
def call(to, subject, body)
|
79
|
+
@data = Data.new(@from, to, subject, body)
|
80
|
+
|
81
|
+
@smtp.start(Socket.gethostname, @user, @password, :plain) do |smtp|
|
82
|
+
smtp.send_message(@data.to_s, extract_addr(@from), extract_addrs(to))
|
83
|
+
end
|
84
|
+
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def initialize_smtp
|
91
|
+
@smtp = Net::SMTP.new(@host, @port)
|
92
|
+
maybe_enable_security(@security)
|
93
|
+
end
|
94
|
+
|
95
|
+
def maybe_enable_security(security)
|
96
|
+
case security
|
97
|
+
when 'tls' then @smtp.enable_tls
|
98
|
+
when 'starttls' then @smtp.enable_starttls
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def extract_addr(str)
|
103
|
+
match = str.match(%r{<([^/]+)>})
|
104
|
+
return match[1] if match
|
105
|
+
|
106
|
+
str
|
107
|
+
end
|
108
|
+
|
109
|
+
def extract_addrs(to)
|
110
|
+
return to.map{ |addr| extract_addr(addr) } if to.instance_of?(Array)
|
111
|
+
|
112
|
+
extract_addr(to)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Deliver email.
|
117
|
+
#
|
118
|
+
# @param to [Array, String] In the format `Name <user@mail>` or `user@mail`.
|
119
|
+
# @param subject [String]
|
120
|
+
# @param body [String]
|
121
|
+
# @return [Deliver]
|
122
|
+
def self.deliver(to, subject, body)
|
123
|
+
LeanMail::Deliver.new.call(to, subject, body)
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
|
+
#
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
|
+
|
10
|
+
|
11
|
+
module LeanWeb
|
12
|
+
module ControllerMixins
|
13
|
+
# {render_with_layout} for {Controller} with ERB, HAML, Tilt::EmacsOrg and
|
14
|
+
# Redcarpet Markdown support.
|
15
|
+
module RenderWithLayout
|
16
|
+
# Render a response with a layout.
|
17
|
+
#
|
18
|
+
# Depending on the `path` file contents and options `@content_for` might
|
19
|
+
# be filled. These contents must be inserted on the `head` or `body` of
|
20
|
+
# your layout file.
|
21
|
+
#
|
22
|
+
# All paths used on parameters can be relative to {VIEW_PATH} or absolute.
|
23
|
+
#
|
24
|
+
# @param path [String] Path to file to rendered.
|
25
|
+
# @param title [String] `@content_for[:title]` content.
|
26
|
+
# @param head [Hash] Things to be inserted on `@content_for[:head]`.
|
27
|
+
# @option head [String|Array] :js Javascript file path(s).
|
28
|
+
# @option head [String|Array] :jsm Javascript module file path (s).
|
29
|
+
# @option head [String|Array] :css CSS file path(s).
|
30
|
+
# @option head [String|Array] :raw Raw headers in HTML format.
|
31
|
+
# @param layout [String] Layout file path.
|
32
|
+
# @param sub_layout [String] Layout file to be inserted within layout.
|
33
|
+
# @param options [Hash] Options hash, to be given to the template engine
|
34
|
+
# based on file extension: `Tilt::EmacsOrg` for `.org` and `Redcarpet`
|
35
|
+
# for `.md`.
|
36
|
+
# @option options [String] :setupfile For `.org`.
|
37
|
+
# @option options [true] :toc For `.md`. To fill `@content_for[:toc]`
|
38
|
+
# render with option `:with_toc_data`.
|
39
|
+
# @yield On dynamic templates (such as Haml and ERB) you can yield.
|
40
|
+
def render_with_layout( # rubocop:disable Metrics/ParameterLists
|
41
|
+
path,
|
42
|
+
title: nil,
|
43
|
+
head: nil,
|
44
|
+
layout: 'layout.haml',
|
45
|
+
sub_layout: nil,
|
46
|
+
options: {},
|
47
|
+
&block
|
48
|
+
)
|
49
|
+
@content_for[:title] = title unless title.nil?
|
50
|
+
content = render_by_extension(path, sub_layout, options, &block)
|
51
|
+
prepare_head(head) unless head.nil?
|
52
|
+
render_response(layout, 'text/html'){ content }
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
# @param path [String]
|
58
|
+
# @param layout [String]
|
59
|
+
# @param options [Hash] Check `Tilt::EmacsOrg` options.
|
60
|
+
# @option options [String] :setupfile can be absolute or relative to
|
61
|
+
# {VIEW_PATH}.
|
62
|
+
def render_org(path, layout: nil, options: {})
|
63
|
+
options[:setupfile] = "#{VIEW_PATH}/#{options[:setupfile]}" \
|
64
|
+
if options.include?(:setupfile) && options[:setupfile][0] != '/'
|
65
|
+
|
66
|
+
org_template = create_template(path, options)
|
67
|
+
|
68
|
+
@content_for[:title] = org_template.title \
|
69
|
+
unless @content_for.include?(:title)
|
70
|
+
|
71
|
+
if layout
|
72
|
+
create_template(layout).render(self){ org_template.render(self) }
|
73
|
+
else
|
74
|
+
org_template.render(self)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param path [String]
|
79
|
+
# @param layout [String]
|
80
|
+
# @param options [Hash] Check `RedCarpet` `render_options`.
|
81
|
+
# @option options [true] :toc To place an HTML based table of contents on
|
82
|
+
# `@content_for[:toc]` and render with option `:with_toc_data`.
|
83
|
+
def render_markdown(path, layout: nil, options: {})
|
84
|
+
maybe_render_markdown_toc!(path, options)
|
85
|
+
markdown_template = create_template(path, options)
|
86
|
+
if layout
|
87
|
+
create_template(layout).render(self){ markdown_template.render(self) }
|
88
|
+
else
|
89
|
+
markdown_template.render(self)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# If `options[:toc]` is set, deletes options `:toc` and adds
|
94
|
+
# `:with_toc_data`. Also adds `@content_for[:toc]`.
|
95
|
+
def maybe_render_markdown_toc!(path, options)
|
96
|
+
return unless options.include?(:toc)
|
97
|
+
|
98
|
+
require('redcarpet')
|
99
|
+
@content_for[:toc] = create_template(
|
100
|
+
path, { renderer: Redcarpet::Render::HTML_TOC }
|
101
|
+
).render
|
102
|
+
|
103
|
+
options.delete(:toc)
|
104
|
+
options[:with_toc_data] = true
|
105
|
+
end
|
106
|
+
|
107
|
+
# @param path [String]
|
108
|
+
# @param layout [String]
|
109
|
+
# @param options [Hash] Check `Haml` options.
|
110
|
+
# @yield If block given.
|
111
|
+
def render_other(path, layout: nil, options: {})
|
112
|
+
template = create_template(path, options)
|
113
|
+
if layout
|
114
|
+
create_template(layout).render(self) do
|
115
|
+
template.render(self){ yield if block_given? }
|
116
|
+
end
|
117
|
+
else
|
118
|
+
template.render(self){ yield if block_given? }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def render_by_extension(path, layout, options, &block)
|
123
|
+
case File.extname(path)
|
124
|
+
when '.org'
|
125
|
+
render_org(path, layout: layout, options: options)
|
126
|
+
when '.md'
|
127
|
+
render_markdown(path, layout: layout, options: options)
|
128
|
+
else
|
129
|
+
render_other(path, layout: layout, options: options, &block)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def prepare_head(js: nil, jsm: nil, css: nil, raw: nil)
|
134
|
+
head = String.new
|
135
|
+
head << prepare_head_js(js) unless js.nil?
|
136
|
+
head << prepare_head_js(jsm, 'module') unless jsm.nil?
|
137
|
+
head << prepare_head_css(css) unless css.nil?
|
138
|
+
head << prepare_head_raw(raw) unless raw.nil?
|
139
|
+
@content_for[:head] = head
|
140
|
+
end
|
141
|
+
|
142
|
+
def prepare_head_js(js, type = 'application/javascript')
|
143
|
+
head = String.new
|
144
|
+
js = [js] if js.instance_of?(String)
|
145
|
+
js.each do |src|
|
146
|
+
head << "<script type='#{type}' src='#{src}'"
|
147
|
+
head << ' defer' if type != 'module'
|
148
|
+
head << "></script>\n"
|
149
|
+
end
|
150
|
+
head
|
151
|
+
end
|
152
|
+
|
153
|
+
def prepare_head_css(css)
|
154
|
+
head = String.new
|
155
|
+
css = [css] if css.instance_of?(String)
|
156
|
+
css.each do |src|
|
157
|
+
head << "<link rel='stylesheet' type='text/css' href='#{src}'>\n"
|
158
|
+
end
|
159
|
+
head
|
160
|
+
end
|
161
|
+
|
162
|
+
def prepare_head_raw(raw)
|
163
|
+
head = String.new
|
164
|
+
raw = [raw] if raw.instance_of?(String)
|
165
|
+
raw.each{ |src| head << "#{src}\n" }
|
166
|
+
head
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
|
+
#
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
|
+
|
10
|
+
require 'date'
|
11
|
+
require 'fileutils'
|
12
|
+
require 'json'
|
13
|
+
require 'rack/session/abstract/id'
|
14
|
+
|
15
|
+
module Rack
|
16
|
+
module Session
|
17
|
+
# A JSON File based session storage.
|
18
|
+
class JsonFile < Abstract::PersistedSecure
|
19
|
+
# Cleans session files that have not been used.
|
20
|
+
# @param to [Time|Date] Time from which file has not been accessed.
|
21
|
+
# Defaults to 00:00 hrs 1 month ago.
|
22
|
+
# @param dir [String] Directory from which to clean files. Defaults to
|
23
|
+
# `/tmp/rack.session`.
|
24
|
+
def self.clean(
|
25
|
+
to = Date.today.prev_month,
|
26
|
+
dir = "#{Dir.tmpdir}/rack.session"
|
27
|
+
)
|
28
|
+
to = to.to_time if to.instance_of?(Date)
|
29
|
+
Dir["#{dir}/*"].each do |file|
|
30
|
+
FileUtils.rm(file, force: true) if ::File.atime(file) < to
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param options [Hash] Accepts a :session_dir path which defaults to
|
35
|
+
# `/tmp/rack.session`.
|
36
|
+
def initialize(app, options = {})
|
37
|
+
super(
|
38
|
+
app,
|
39
|
+
{
|
40
|
+
secure: ENV['RACK_ENV'] != 'development', # HTTPS unless in dev env
|
41
|
+
same_site: 'Strict' # don't allow cross-site sessions
|
42
|
+
}.merge!(options)
|
43
|
+
)
|
44
|
+
@session_dir = "#{Dir.tmpdir}/#{@key}" || @default_options[:session_dir]
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Array] [self, Hash].
|
48
|
+
def find_session(_req, sid)
|
49
|
+
sid = generate_sid if sid.nil?
|
50
|
+
|
51
|
+
[sid, JSON.parse(::File.read("#{@session_dir}/#{sid}"))]
|
52
|
+
rescue Errno::ENOENT
|
53
|
+
[generate_sid, {}]
|
54
|
+
rescue JSON::ParserError
|
55
|
+
[sid, {}]
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param sid [String] Only known ids are allowed, to avoid session
|
59
|
+
# fixation attacks.
|
60
|
+
# @return [self|false] session_id or false.
|
61
|
+
def write_session(_req, sid, session, _options)
|
62
|
+
return false unless ::File.exist?("#{@session_dir}/#{sid}")
|
63
|
+
|
64
|
+
::File.open("#{@session_dir}/#{sid}", 'w', 0o600) do |f|
|
65
|
+
f << JSON.generate(session)
|
66
|
+
end
|
67
|
+
sid
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [self|nil] new session id or nil if options[:drop].
|
71
|
+
def delete_session(_req, sid, options)
|
72
|
+
FileUtils.rm("#{@session_dir}/#{sid}", force: true)
|
73
|
+
options&.include?(:drop) ? nil : generate_sid
|
74
|
+
end
|
75
|
+
|
76
|
+
def generate_sid(*)
|
77
|
+
sid = super
|
78
|
+
FileUtils.mkdir_p(@session_dir, mode: 0o700)
|
79
|
+
::File.new("#{@session_dir}/#{sid}", 'w', 0o600).close
|
80
|
+
sid
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/leanweb/app.rb
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
4
|
#
|
5
|
-
# This file is part of "LeanWeb" and licensed under the terms of the
|
6
|
-
# General Public License version
|
7
|
-
# should have received a copy of this
|
8
|
-
# see <https://
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
9
|
|
10
10
|
require 'rack'
|
11
11
|
|
data/lib/leanweb/controller.rb
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
4
|
#
|
5
|
-
# This file is part of "LeanWeb" and licensed under the terms of the
|
6
|
-
# General Public License version
|
7
|
-
# should have received a copy of this
|
8
|
-
# see <https://
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
9
|
|
10
10
|
require 'rack'
|
11
11
|
require 'tilt'
|
@@ -24,6 +24,7 @@ module LeanWeb
|
|
24
24
|
@route = route
|
25
25
|
@request = request
|
26
26
|
@response = Rack::Response.new(nil, 200)
|
27
|
+
@content_for = {}
|
27
28
|
end
|
28
29
|
|
29
30
|
# Render magic. Supports every file extension that Tilt supports. Defaults
|
data/lib/leanweb/helpers.rb
CHANGED
@@ -2,23 +2,33 @@
|
|
2
2
|
|
3
3
|
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
4
|
#
|
5
|
-
# This file is part of "LeanWeb" and licensed under the terms of the
|
6
|
-
# General Public License version
|
7
|
-
# should have received a copy of this
|
8
|
-
# see <https://
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
9
|
|
10
|
-
# String helpers.
|
11
|
-
class String
|
12
|
-
# String to PascalCase.
|
13
|
-
def pascalize
|
14
|
-
camelize(pascal: true)
|
15
|
-
end
|
16
10
|
|
17
|
-
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
11
|
+
module LeanWeb
|
12
|
+
# String refinements.
|
13
|
+
module StringRefinements
|
14
|
+
refine ::String do
|
15
|
+
# String to PascalCase.
|
16
|
+
def pascalize
|
17
|
+
camelize(pascal: true)
|
18
|
+
end
|
19
|
+
|
20
|
+
# String to camelCase.
|
21
|
+
# @param pascal [Boolean] If true first letter is uppercase.
|
22
|
+
def camelize(pascal: false)
|
23
|
+
str = gsub(/[-_\s]+(.?)/){ |match| match[1].upcase }
|
24
|
+
str[0] = pascal ? str[0].upcase : str[0].downcase
|
25
|
+
str
|
26
|
+
end
|
27
|
+
|
28
|
+
# String to snake_case
|
29
|
+
def snakeize
|
30
|
+
gsub(/[[:upper:]]/){ |match| "_#{match.downcase}" }.delete_prefix('_')
|
31
|
+
end
|
32
|
+
end
|
23
33
|
end
|
24
34
|
end
|
data/lib/leanweb/route.rb
CHANGED
@@ -2,20 +2,22 @@
|
|
2
2
|
|
3
3
|
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
4
|
#
|
5
|
-
# This file is part of "LeanWeb" and licensed under the terms of the
|
6
|
-
# General Public License version
|
7
|
-
# should have received a copy of this
|
8
|
-
# see <https://
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
9
|
|
10
10
|
require 'rack/mock'
|
11
11
|
|
12
12
|
module LeanWeb
|
13
13
|
# Action for {Route#action}.
|
14
|
-
Action = Struct.new(:
|
14
|
+
Action = Struct.new(:controller, :action, keyword_init: true)
|
15
15
|
|
16
16
|
# A single route which routes with the {#respond} method. It can also {#build}
|
17
17
|
# an static file.
|
18
18
|
class Route
|
19
|
+
using StringRefinements
|
20
|
+
|
19
21
|
attr_reader :path, :method, :action, :static
|
20
22
|
|
21
23
|
# A new instance of Route.
|
@@ -27,13 +29,13 @@ module LeanWeb
|
|
27
29
|
# @param action [Proc, Hash, String, nil] References a Method/Proc to be
|
28
30
|
# triggered. It can be:
|
29
31
|
#
|
30
|
-
# -
|
31
|
-
#
|
32
|
-
#
|
33
|
-
# - A
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
32
|
+
# - Nothing, defaults to :"{#path_basename}_{#method}" (such as
|
33
|
+
# `:index_get`) on `:MainController`.
|
34
|
+
# - A full hash `{ controller: 'val', action: 'val' }`.
|
35
|
+
# - A hash with `{ Controller: :action }` only.
|
36
|
+
# - A simple string for an action on `:MainController`.
|
37
|
+
# - It can also be a `Proc`. Will be executed in a `:MainController`
|
38
|
+
# instance context.
|
37
39
|
#
|
38
40
|
# @param static [Boolean|Array] Defines a route as static. Defaults true for
|
39
41
|
# GET method, false otherwise. Set to `false` to say it can only work
|
@@ -108,42 +110,45 @@ module LeanWeb
|
|
108
110
|
|
109
111
|
# Assign value to `@action`.
|
110
112
|
def action=(value)
|
111
|
-
@action =
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
113
|
+
@action =
|
114
|
+
if value.instance_of?(Proc)
|
115
|
+
value
|
116
|
+
else
|
117
|
+
Action.new(**prepare_action_hash(value))
|
118
|
+
end
|
116
119
|
end
|
117
120
|
|
118
121
|
# @param src_value [Hash, String, nil] Check {#initialize} action param for
|
119
122
|
# valid input.
|
120
123
|
# @return [Hash] valid hash for {Action}.
|
121
|
-
def prepare_action_hash(src_value)
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
else
|
126
|
-
value = {}
|
127
|
-
value[:file], value[:action] = src_value.first
|
128
|
-
end
|
129
|
-
rescue NoMethodError
|
130
|
-
value = { action: src_value }
|
131
|
-
end
|
132
|
-
value[:file] ||= 'main'
|
133
|
-
value[:controller] ||= "#{value[:file].pascalize}Controller"
|
134
|
-
value[:action] ||= default_action_name
|
124
|
+
def prepare_action_hash(src_value)
|
125
|
+
value = prepare_prepare_action_hash(src_value)
|
126
|
+
value[:controller] = value[:controller]&.to_sym || DEFAULT_CONTROLLER
|
127
|
+
value[:action] = value[:action]&.to_sym || default_action_action
|
135
128
|
value
|
136
129
|
end
|
137
130
|
|
138
|
-
def
|
139
|
-
|
131
|
+
def prepare_prepare_action_hash(src_value)
|
132
|
+
if %i[controller action].include?(src_value.keys.first)
|
133
|
+
src_value
|
134
|
+
else
|
135
|
+
value = {}
|
136
|
+
value[:controller], value[:action] = src_value.first
|
137
|
+
value
|
138
|
+
end
|
139
|
+
rescue NoMethodError
|
140
|
+
{ action: src_value }
|
141
|
+
end
|
142
|
+
|
143
|
+
def default_action_action
|
144
|
+
"#{path_basename.gsub('-', '_')}_#{@method.downcase}".to_sym
|
140
145
|
end
|
141
146
|
|
142
147
|
# @param request [Rack::Request]
|
143
148
|
# @return [Array] a valid Rack response.
|
144
149
|
def respond_method(request)
|
145
150
|
params = action_params(request.path)
|
146
|
-
require_relative("#{
|
151
|
+
require_relative("#{CONTROLLER_PATH}/#{@action.controller.to_s.snakeize}")
|
147
152
|
controller = Object.const_get(@action.controller).new(self, request)
|
148
153
|
return controller.public_send(@action.action, **params) \
|
149
154
|
if params.instance_of?(Hash)
|
@@ -155,9 +160,12 @@ module LeanWeb
|
|
155
160
|
# @return [Array] a valid Rack response.
|
156
161
|
def respond_proc(request)
|
157
162
|
params = action_params(request.path)
|
158
|
-
|
163
|
+
require_relative("#{CONTROLLER_PATH}/#{DEFAULT_CONTROLLER.to_s.snakeize}")
|
164
|
+
controller = Object.const_get(DEFAULT_CONTROLLER).new(self, request)
|
165
|
+
return controller.instance_exec(**params, &@action) \
|
166
|
+
if params.instance_of?(Hash)
|
159
167
|
|
160
|
-
|
168
|
+
controller.instance_exec(*params, &@action)
|
161
169
|
end
|
162
170
|
|
163
171
|
# @param request_path [String]
|
@@ -178,8 +186,18 @@ module LeanWeb
|
|
178
186
|
# @param content_type [String]
|
179
187
|
# @return [String] absolute route to path + extension based on content_type.
|
180
188
|
def output_path(path, content_type)
|
181
|
-
|
182
|
-
|
189
|
+
out_path =
|
190
|
+
if path[-1] == '/'
|
191
|
+
String.new("#{PUBLIC_PATH}#{path}/index")
|
192
|
+
else
|
193
|
+
String.new("#{PUBLIC_PATH}#{path}")
|
194
|
+
end
|
195
|
+
|
196
|
+
unless path.end_with?(MEDIA_EXTENSIONS[content_type])
|
197
|
+
out_path << MEDIA_EXTENSIONS[content_type]
|
198
|
+
end
|
199
|
+
|
200
|
+
out_path
|
183
201
|
end
|
184
202
|
end
|
185
203
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
|
+
#
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
|
+
|
10
|
+
|
11
|
+
module LeanWeb
|
12
|
+
VERSION = '0.3.0'
|
13
|
+
end
|
data/lib/leanweb.rb
CHANGED
@@ -2,14 +2,14 @@
|
|
2
2
|
|
3
3
|
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
4
|
#
|
5
|
-
# This file is part of "LeanWeb" and licensed under the terms of the
|
6
|
-
# General Public License version
|
7
|
-
# should have received a copy of this
|
8
|
-
# see <https://
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
9
|
|
10
10
|
# LeanWeb is a minimal hybrid static / dynamic web framework.
|
11
11
|
module LeanWeb
|
12
|
-
|
12
|
+
require_relative 'leanweb/version'
|
13
13
|
|
14
14
|
ROOT_PATH = ENV.fetch('LEANWEB_ROOT_PATH', Dir.pwd)
|
15
15
|
CONTROLLER_PATH = "#{ROOT_PATH}/src/controllers"
|
@@ -26,13 +26,11 @@ module LeanWeb
|
|
26
26
|
'text/csv' => '.csv'
|
27
27
|
}.freeze
|
28
28
|
|
29
|
+
DEFAULT_CONTROLLER = :MainController
|
30
|
+
|
29
31
|
autoload :Route, 'leanweb/route.rb'
|
30
32
|
autoload :Controller, 'leanweb/controller.rb'
|
31
33
|
autoload :App, 'leanweb/app.rb'
|
32
34
|
end
|
33
35
|
|
34
36
|
require_relative 'leanweb/helpers'
|
35
|
-
|
36
|
-
# TODO: Remove if/when tilt-emacs_org gets merged on a Tilt release.
|
37
|
-
require 'tilt'
|
38
|
-
Tilt.register_lazy(:EmacsOrgTemplate, 'tilt/emacs_org', 'org')
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: leanweb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Felix Freeman
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 2.0.
|
33
|
+
version: 2.0.11
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 2.0.
|
40
|
+
version: 2.0.11
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: haml
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 5.16.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest-sprint
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.2.2
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.2.2
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: rake
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,6 +136,34 @@ dependencies:
|
|
122
136
|
- - "~>"
|
123
137
|
- !ruby/object:Gem::Version
|
124
138
|
version: 0.20.1
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: tilt-emacs_org
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 0.1.1
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 0.1.1
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: webmock
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '3.18'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '3.18'
|
125
167
|
- !ruby/object:Gem::Dependency
|
126
168
|
name: yard
|
127
169
|
requirement: !ruby/object:Gem::Requirement
|
@@ -145,14 +187,19 @@ extensions: []
|
|
145
187
|
extra_rdoc_files: []
|
146
188
|
files:
|
147
189
|
- bin/leanweb
|
190
|
+
- contrib/lib/hawese.rb
|
191
|
+
- contrib/lib/lean_mail.rb
|
192
|
+
- contrib/lib/leanweb/controller_mixins/render_with_layout.rb
|
193
|
+
- contrib/lib/rack/session/json_file.rb
|
148
194
|
- lib/leanweb.rb
|
149
195
|
- lib/leanweb/app.rb
|
150
196
|
- lib/leanweb/controller.rb
|
151
197
|
- lib/leanweb/helpers.rb
|
152
198
|
- lib/leanweb/route.rb
|
199
|
+
- lib/leanweb/version.rb
|
153
200
|
homepage: https://leanweb.hacktivista.org
|
154
201
|
licenses:
|
155
|
-
-
|
202
|
+
- AGPL-3.0-only
|
156
203
|
metadata:
|
157
204
|
homepage_uri: https://leanweb.hacktivista.org
|
158
205
|
source_code_uri: https://git.hacktivista.org/leanweb
|
@@ -163,6 +210,7 @@ post_install_message:
|
|
163
210
|
rdoc_options: []
|
164
211
|
require_paths:
|
165
212
|
- lib
|
213
|
+
- contrib/lib
|
166
214
|
required_ruby_version: !ruby/object:Gem::Requirement
|
167
215
|
requirements:
|
168
216
|
- - ">="
|
@@ -174,7 +222,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
174
222
|
- !ruby/object:Gem::Version
|
175
223
|
version: '0'
|
176
224
|
requirements: []
|
177
|
-
rubygems_version: 3.
|
225
|
+
rubygems_version: 3.3.21
|
178
226
|
signing_key:
|
179
227
|
specification_version: 4
|
180
228
|
summary: LeanWeb is a minimal hybrid static / dynamic web framework
|