waxx 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE +201 -0
  4. data/README.md +879 -0
  5. data/bin/waxx +120 -0
  6. data/lib/waxx/app.rb +173 -0
  7. data/lib/waxx/conf.rb +54 -0
  8. data/lib/waxx/console.rb +204 -0
  9. data/lib/waxx/csrf.rb +14 -0
  10. data/lib/waxx/database.rb +80 -0
  11. data/lib/waxx/encrypt.rb +38 -0
  12. data/lib/waxx/error.rb +60 -0
  13. data/lib/waxx/html.rb +33 -0
  14. data/lib/waxx/http.rb +268 -0
  15. data/lib/waxx/init.rb +273 -0
  16. data/lib/waxx/irb.rb +44 -0
  17. data/lib/waxx/irb_env.rb +18 -0
  18. data/lib/waxx/json.rb +23 -0
  19. data/lib/waxx/mongodb.rb +221 -0
  20. data/lib/waxx/mysql2.rb +234 -0
  21. data/lib/waxx/object.rb +115 -0
  22. data/lib/waxx/patch.rb +138 -0
  23. data/lib/waxx/pdf.rb +69 -0
  24. data/lib/waxx/pg.rb +246 -0
  25. data/lib/waxx/process.rb +270 -0
  26. data/lib/waxx/req.rb +116 -0
  27. data/lib/waxx/res.rb +98 -0
  28. data/lib/waxx/server.rb +304 -0
  29. data/lib/waxx/sqlite3.rb +237 -0
  30. data/lib/waxx/supervisor.rb +47 -0
  31. data/lib/waxx/test.rb +162 -0
  32. data/lib/waxx/util.rb +57 -0
  33. data/lib/waxx/version.rb +3 -0
  34. data/lib/waxx/view.rb +389 -0
  35. data/lib/waxx/waxx.rb +73 -0
  36. data/lib/waxx/x.rb +103 -0
  37. data/lib/waxx.rb +50 -0
  38. data/skel/README.md +11 -0
  39. data/skel/app/app/app.rb +39 -0
  40. data/skel/app/app/error/app_error.rb +16 -0
  41. data/skel/app/app/error/dhtml.rb +9 -0
  42. data/skel/app/app/error/html.rb +8 -0
  43. data/skel/app/app/error/json.rb +8 -0
  44. data/skel/app/app/error/pdf.rb +13 -0
  45. data/skel/app/app/log/app_log.rb +13 -0
  46. data/skel/app/app.rb +20 -0
  47. data/skel/app/home/home.rb +16 -0
  48. data/skel/app/home/html.rb +145 -0
  49. data/skel/app/html.rb +192 -0
  50. data/skel/app/usr/email.rb +66 -0
  51. data/skel/app/usr/html.rb +115 -0
  52. data/skel/app/usr/list.rb +51 -0
  53. data/skel/app/usr/password.rb +54 -0
  54. data/skel/app/usr/record.rb +98 -0
  55. data/skel/app/usr/usr.js +67 -0
  56. data/skel/app/usr/usr.rb +277 -0
  57. data/skel/app/waxx/waxx.rb +109 -0
  58. data/skel/bin/README.md +1 -0
  59. data/skel/db/README.md +11 -0
  60. data/skel/db/app/0-init.sql +88 -0
  61. data/skel/lib/README.md +1 -0
  62. data/skel/log/README.md +1 -0
  63. data/skel/opt/dev/config.yaml +1 -0
  64. data/skel/opt/prod/config.yaml +1 -0
  65. data/skel/opt/stage/config.yaml +1 -0
  66. data/skel/opt/test/config.yaml +1 -0
  67. data/skel/private/README.md +1 -0
  68. data/skel/public/lib/site.css +202 -0
  69. data/skel/public/lib/waxx/w.ico +0 -0
  70. data/skel/public/lib/waxx/w.png +0 -0
  71. data/skel/public/lib/waxx/waxx.js +111 -0
  72. data/skel/tmp/pids/README.md +1 -0
  73. data.tar.gz.sig +0 -0
  74. metadata +140 -0
  75. metadata.gz.sig +3 -0
@@ -0,0 +1,277 @@
1
+ module App::Usr
2
+ extend Waxx::Pg
3
+ extend self
4
+
5
+ has(
6
+ id: {type: "integer",label:"ID"},
7
+ usr_name: {type: "character",label:""},
8
+ password_sha256: {type: "character",label:""},
9
+ salt_aes256: {type: "character",label:""},
10
+ failed_login_count: {type: "smallint",label:""},
11
+ require_new_password: {type: "smallint",label:""},
12
+ password_mod_date: {type: "date",label:""},
13
+ last_login_host: {type: "character",label:""},
14
+ last_login_date: {type: "timestamp",label:""},
15
+ create_date: {type: "timestamp",label:""},
16
+ mod_date: {type: "timestamp",label:""},
17
+ create_by_id: {type: "integer",label:""},
18
+ mod_by_id: {type: "integer",label:""},
19
+ key: {type: "character",label:""},
20
+ key_sent_date: {type: "timestamp",label:""}
21
+ )
22
+
23
+ # Create the first usr (or any usr). Run from the console: App::Usr.init(x)
24
+ def init(x)
25
+ data = {}
26
+ puts "Create a new user"
27
+ print "Email: "
28
+ data[:usr_name] = gets.chomp
29
+ print "Password: "
30
+ data[:password] = gets.chomp
31
+ salt, encrypted_password = salt_password(x, data/:password)
32
+ data[:salt_aes256] = salt
33
+ data[:password_sha256] = encrypted_password
34
+ u = post(x, data, returning: 'id')
35
+ puts "User #{data/:usr_name} added (usr.id: #{u['id']})"
36
+ end
37
+
38
+ def login(x, usr_name, password)
39
+ u = usr(x, usr_name:usr_name)
40
+ return false, "Invalid user name", {} if u.nil? or u['id'].nil?
41
+ pass = password?(u, password, u['salt_aes256'])
42
+ return false, "Invalid password", {} if pass == false
43
+ login_successful(x, u)
44
+ return true, "Login successful", u
45
+ end
46
+
47
+ def record(x, id:0)
48
+ get_by_id(x, id, "id, usr_name, password_sha256, last_login_date, last_login_host, password_mod_date")
49
+ end
50
+
51
+ def usr(x, usr_name:'', id:0)
52
+ x.db.exec("
53
+ SELECT id, company_id, usr_name, password_sha256, salt_aes256, last_login_date, last_login_host
54
+ FROM usr
55
+ WHERE usr_name = $1
56
+ OR usr.id = $2
57
+ LIMIT 1", [usr_name, id]).first #rescue {}
58
+ end
59
+
60
+ def password?(u, password, salt)
61
+ #u['password_sha256'] == Digest::SHA256.hexdigest(App.decrypt(salt) + password)
62
+ u['password_sha256'] == Digest::SHA256.hexdigest(Conf['encryption']['old_key'] + password)
63
+ end
64
+
65
+ def salt_password(x, password)
66
+ salt = Waxx.random_string(32, :any)
67
+ [App.encrypt(salt), Digest::SHA256.hexdigest(salt + password)]
68
+ end
69
+
70
+ def set_password(x, id, password)
71
+ salt, epw = salt_password(x, password)
72
+ put(x, id, {salt_aes256: salt, password_sha256: epw})
73
+ end
74
+
75
+ def create(x, data={}, returning: "id")
76
+ data = blk.call if block_given?
77
+ salt, encrypted_password = salt_password(x, data/:password)
78
+ data[:salt_aes256] = salt
79
+ data[:password_sha256] = encrypted_password
80
+ post(x, data, returning: returning)
81
+ end
82
+
83
+ def login_successful(x, usr)
84
+ Waxx.debug "login_successful"
85
+ x.db.exec("
86
+ update usr set
87
+ last_login_date = now(),
88
+ last_login_host = $1,
89
+ failed_login_count = 0
90
+ where usr_name = $2",
91
+ [
92
+ x.req.env['X-Forwarded-For'],
93
+ usr['usr_name']
94
+ ]
95
+ )
96
+ set_cookies(x, usr)
97
+ debug x.ua.inspect
98
+ end
99
+
100
+ def key_for_reset(x, id)
101
+ x.db.exec("
102
+ update usr set
103
+ key = generate_key(),
104
+ key_sent_date = now()
105
+ where id = $1
106
+ returning key",
107
+ [ id ]
108
+ ).first['key']
109
+ end
110
+
111
+ def set_cookies(x, usr)
112
+ x.usr['id'] = usr['id']
113
+ x.usr['cid'] = usr['company_id']
114
+ x.usr['grp'] = groups(x, usr['id']).push("user")
115
+ x.usr['uk'] = Waxx.random_string(20) # The Update Key is used to protect against CSRF
116
+ x.usr['la'] = Time.now.to_i # Last Activity used for session expiration
117
+ x.ua['id'] = usr['id']
118
+ x.ua['cid'] = usr['company_id']
119
+ x.ua['un'] = usr['usr_name']
120
+ x.ua['rm'] = x['remember_me'].to_i # Remember the user name for the login or UI features
121
+ x.ua['ll'] = Time.now.to_i # Last Login used for session expiration and welcome back
122
+ end
123
+
124
+ def groups(x, usr_id)
125
+ x.db.exec("
126
+ SELECT name
127
+ FROM grp JOIN usr_grp ON grp.id = usr_grp.grp_id
128
+ WHERE usr_grp.usr_id = $1",[usr_id]).column_values(0)
129
+ end
130
+
131
+ runs(
132
+ default: "list",
133
+ home: {
134
+ desc: "The home page of a logged in usr",
135
+ acl: "user",
136
+ get: proc{|x, *args|
137
+ usr = App::Usr.record(x, id: x.usr['id'])
138
+ person = App::Person.record(x, id: x.usr['id']).first
139
+ App::Html.render(x,
140
+ title: "#{person['first_name'].h} #{person['last_name'].h}",
141
+ content: App::Usr::Html.home(x, usr: usr, person: person)
142
+ )
143
+ }
144
+ },
145
+ list: {
146
+ desc: "List users",
147
+ acl: %w(admin),
148
+ get: lambda{|x| List.run(x) }
149
+ },
150
+ record: {
151
+ desc: "Edit a usr record",
152
+ acl: %w(admin),
153
+ get: lambda{|x, id, *args| Record.run(x, id:id) },
154
+ post: lambda{|x, id, *args| Record.save(x, id:id, data:x.req.post) }
155
+ },
156
+ login: {
157
+ desc: "Login",
158
+ get: proc{|x, *args|
159
+ #App::Html.page(x, title: "Login to #{Conf['site']['name']}", content: App::Usr::Html.login(x))
160
+ x.res.redirect "/app/login?return_to=#{Waxx::Html.qs x.req.uri}"
161
+ },
162
+ post: proc{|x, *args|
163
+ success, message, u = login(x, x['usr_name'], x['password'])
164
+ if success
165
+ if x.ext == 'json'
166
+ x.res['Content-Type'] = "application/json"
167
+ x << { success: success, message: message, usr: usr, key: key }.to_json
168
+ else
169
+ if x['return_to'].to_s[0] == '/'
170
+ x << %(<html><script>location = '#{x['return_to']}';</script></html>)
171
+ else
172
+ x << %(<html>Error: Must return to a local path. Attempted return_to: #{x['return_to'].to_s.h}</html>)
173
+ end
174
+ # Some browsers not storing cookies on redirect
175
+ #x.res.status = 302
176
+ #x.res['Location'] = x['return_to']
177
+ end
178
+ else
179
+ if x.ext == 'json'
180
+ x.res['Content-Type'] = "application/json"
181
+ x << { success: success, message: message, usr: usr, key: key }.to_json
182
+ else
183
+ App::Html.page(x, title: "Login Error", message:{type:"danger", message: message}, content: App::Usr::Html.login(x, return_to:x['return_to']))
184
+ end
185
+ end
186
+ }
187
+ },
188
+ logout: {
189
+ desc: "Logout of the app",
190
+ get: proc{|x|
191
+ x.usr['id'] = nil
192
+ x.usr['grp'] = nil
193
+ if x.ext == "json"
194
+ x << %({"success":true})
195
+ else
196
+ x.res.status = 302
197
+ x.res['Location'] = '/'
198
+ end
199
+ }
200
+ },
201
+ password_reset:{
202
+ desc: "Send the user a password reset link.",
203
+ post: proc{|x, *args|
204
+ # See if the user exists
205
+ u = App::Usr.usr(x, usr_name: x['email'])
206
+ if u.nil?
207
+ # Send an email that we do not have an account for that user
208
+ App::Email.post(x, Email.email_not_found(x, x['email']))
209
+ else
210
+ # Send the password reset email
211
+ k = App::Usr.key_for_reset(x, u['id'])
212
+ App::Email.post(x, Email.password_reset(x, u, k))
213
+ end
214
+ App::Html.page(x, title:"Password Reset Sent", content:"A link to reset your password has been sent to #{x['email'].h}. Please check your email. (It may be in your Spam or Junk folder.)")
215
+ }
216
+ },
217
+ password: {
218
+ desc: "Form to select a new password",
219
+ get: lambda{|x, id=nil, key=nil, *args|
220
+ if id.nil? and not x.usr?
221
+ return App.login_needed(x)
222
+ end
223
+ if key.nil? and not x.usr?
224
+ return App.login_needed(x)
225
+ end
226
+ if x.usr?
227
+ App::Html.render(x, title: "Reset Password", content:Html.change_password(x))
228
+ else
229
+ u = App::Usr.get_by_id(x, id, "usr_name, key, key_sent_date")
230
+ if u['key'] != key
231
+ return App.error(x, title:"Link Invalid",
232
+ message:"The link you are using does not match our records.
233
+ If you requested a password reset multiple times today,
234
+ please use the most recent link that you received. If that does
235
+ not work, please request another link on the login page.")
236
+ end
237
+ if u['key_sent_date'] < Time.new - 21600 # 6 hours
238
+ return App.error(x, title:"Link Expired",
239
+ message:"The link you are using expired after 6 hours to protect
240
+ your account. Please request another link on the login page.")
241
+ end
242
+ App::Html.page(x, title: "Reset Password", content:Html.change_password(x, u, key))
243
+ end
244
+ },
245
+ post: lambda{|x, id=nil, key=nil, *args|
246
+ if x['password1'] != x['password2']
247
+ return App::Html.render(x, title: "Reset Password", content:Html.password_form(x), message:{type:"danger", message:"Your passwords do not match. Please try again."})
248
+ end
249
+ if x['password1'] =~ /[a-z]|[A-Z]/
250
+ return App::Html.render(x, title: "Reset Password", content:Html.password_form(x), message:{type:"danger", message:"Your passwords do not match. Please try again."})
251
+ end
252
+ if id.nil? and x.usr?
253
+ App::Usr.set_password(x, x.usr['id'], x["password1"])
254
+ App::Html.page(x, title:"Password Reset Successful", content:"Your password has been reset.")
255
+ elsif id and key
256
+ u = App::Usr.get_by_id(x, id, "usr_name, key, key_sent_date")
257
+ if u['key'] != key
258
+ return App.error(x, title:"Link Invalid",
259
+ message:"The link you are using does not match our records.
260
+ If you requested a password reset multiple times today,
261
+ please use the most recent link that you received. If that does
262
+ not work, please request another link on the login page.")
263
+ end
264
+ App::Usr.set_password(x, id, x["password1"])
265
+ success, message, u = App::Usr.login(x, u['usr_name'], x['password1'])
266
+ App::Html.page(x, title:"Password Reset Successful", content:"Your password has been reset and you have been logged in.")
267
+ end
268
+ }
269
+ }
270
+ )
271
+ end
272
+
273
+ require_relative 'html'
274
+ require_relative 'email'
275
+ require_relative 'password'
276
+ require_relative 'record'
277
+ require_relative 'list'
@@ -0,0 +1,109 @@
1
+ module App::Waxx
2
+ extend Waxx::Object
3
+ extend self
4
+
5
+ runs(
6
+ ok: {
7
+ desc: "Ping the app database to see if it is ok",
8
+ get: -> (x) {
9
+ x << (x.db.app.exec("select 1+1 as two").first['two'] == 2)
10
+ }
11
+ },
12
+ sleep: {
13
+ desc: "Sleep for seconds in args",
14
+ acl: %w(dev),
15
+ run: -> (x, secs=1) {
16
+ sleep(secs.to_i)
17
+ x.res.as :txt
18
+ x << "Done sleeping for #{secs.to_i} seconds"
19
+ }
20
+ },
21
+ error: {
22
+ desc: "Raise an Error (to see what the error looks like and send email if configured)",
23
+ acl: %w(dev),
24
+ run: ->(x, *args){
25
+ raise "test error"
26
+ }
27
+ },
28
+ env: {
29
+ desc: "Output all the input variables",
30
+ acl: "dev",
31
+ run: ->(x, *args) {
32
+ x.res.as :txt
33
+ x << {"x" => {
34
+ "meth" => x.meth,
35
+ "app" => x.app,
36
+ "act" => x.act,
37
+ "oid" => x.oid,
38
+ "args" => x.args,
39
+ "ext" => x.ext,
40
+ "usr" => x.usr,
41
+ "ua" => x.ua,
42
+ "req" => {
43
+ "meth" => x.req.meth,
44
+ "get" => x.req.get,
45
+ "post" => x.req.post,
46
+ "env" => x.req.env,
47
+ "cookies" => x.req.cookies,
48
+ "data" => x.req.data,
49
+ }
50
+ }}.to_yaml
51
+ }
52
+ },
53
+ raw: {
54
+ desc: "Output all the input variables",
55
+ run: -> (x, *args) {
56
+ x.res.as :txt
57
+ x << x.req.env.map{|n,v| "#{n}: #{v}"}.join("\r\n")
58
+ x << "\r\n\r\n"
59
+ x << x.req.data
60
+ }
61
+ },
62
+ desc: {
63
+ desc: "Describes all of the applications interfaces.",
64
+ acl: "dev",
65
+ get: ->(x, app="all"){
66
+ return App.error(x, status: 300, title: 'Request Error', message: 'This method only return json or yaml') unless %w(json yaml).include? x.ext
67
+ re = {}
68
+ describe = -> (ap) {
69
+ re[ap.to_s] = {}
70
+ return nil if App[ap].nil?
71
+ App[ap].each{|act,props|
72
+ re[ap.to_s][act.to_s] = {}
73
+ if props.respond_to?('each')
74
+ props.each{|n, v|
75
+ if Proc === v
76
+ re[ap.to_s][act.to_s][n.to_s] = Hash[v.parameters.map{|param| [param[1].to_s, param[0].to_s]}]
77
+ else
78
+ re[ap.to_s][act.to_s][n.to_s] = v
79
+ end
80
+ }
81
+ else
82
+ re[ap.to_s][act.to_s] = props
83
+ end
84
+ }
85
+ }
86
+ if app == "all"
87
+ App.runs.keys.each{|k|
88
+ describe[k]
89
+ }
90
+ else
91
+ describe[app]
92
+ end
93
+ x << re.send("to_#{x.ext}")
94
+ }
95
+ },
96
+ threads: {
97
+ desc: "Show the status of all threads",
98
+ acl: "dev",
99
+ get: -> (x) {
100
+ x.res.as :txt
101
+ Thread.list.each{|t|
102
+ next if t[:name] == "main"
103
+ x << "#{t[:name]}: #{t[:status]}\n"
104
+ }
105
+ }
106
+ },
107
+ )
108
+
109
+ end
@@ -0,0 +1 @@
1
+ Put executable files like deploy scripts or maintenance scripts here.
data/skel/db/README.md ADDED
@@ -0,0 +1,11 @@
1
+ waxx migration files are stored in a folderf or each database.
2
+
3
+ To generate a migration file, run:
4
+
5
+ `waxx migration app migration-name`
6
+
7
+ replace `app` with the database name defined in config.yaml
8
+
9
+ To migrate the database run:
10
+
11
+ `waxx migrate` for all databases of `waxx migrate app` for only the app database. Replace `app` with the name of any defined database connection.
@@ -0,0 +1,88 @@
1
+ BEGIN;
2
+ CREATE FUNCTION generate_key() RETURNS character varying
3
+ LANGUAGE sql IMMUTABLE
4
+ AS $$select md5(now()::varchar||random()::varchar||random()::varchar);$$;
5
+ CREATE TABLE app_log (
6
+ id serial primary key,
7
+ date_time timestamp without time zone DEFAULT now(),
8
+ usr_id integer,
9
+ category character varying(32),
10
+ name character varying(64),
11
+ value character varying(254),
12
+ related_id integer,
13
+ ip_address character varying(39),
14
+ user_agent character varying(1000)
15
+ );
16
+ CREATE TABLE email (
17
+ id seriall primary key,
18
+ to_email character varying(254) NOT NULL,
19
+ to_name character varying(254),
20
+ to_person_id integer,
21
+ from_email character varying(254) NOT NULL,
22
+ from_name character varying(254),
23
+ from_person_id integer,
24
+ reply_to_name character varying(254),
25
+ reply_to_email character varying(254),
26
+ subject character varying(254) NOT NULL,
27
+ body_text text,
28
+ body_html text,
29
+ headers text,
30
+ email_type character varying(254),
31
+ document_id integer,
32
+ process_status character varying(32) DEFAULT 'draft'::character varying NOT NULL,
33
+ process_id integer,
34
+ process_start timestamp with time zone,
35
+ process_end timestamp with time zone,
36
+ process_error text,
37
+ create_date timestamp without time zone DEFAULT now() NOT NULL,
38
+ mod_date timestamp without time zone DEFAULT now() NOT NULL,
39
+ create_by_id integer,
40
+ mod_by_id integer,
41
+ cc character varying(4000),
42
+ bcc character varying(4000)
43
+ );
44
+ CREATE TABLE grp (
45
+ id serial primary key,
46
+ name character varying(64),
47
+ description character varying(254),
48
+ create_date timestamp without time zone DEFAULT now(),
49
+ mod_date timestamp without time zone DEFAULT now(),
50
+ create_by_id integer,
51
+ mod_by_id integer,
52
+ CONSTRAINT grp_uniq UNIQUE(name)
53
+ );
54
+ INSERT INTO grp (name, description, create_by_id, mod_by_id)
55
+ VALUES ('admin', 'People who can do everything', 1, 1),
56
+ ('dev', 'People who can do everything else', 1, 1);
57
+ CREATE TABLE usr (
58
+ id serial primary key,
59
+ usr_name character varying(254) NOT NULL,
60
+ password_sha256 character varying(64),
61
+ salt_aes256 character varying(254),
62
+ failed_login_count smallint DEFAULT 0 NOT NULL,
63
+ require_new_password boolean NOT NULL DEFAULT false,
64
+ password_mod_date date DEFAULT now() NOT NULL,
65
+ last_login_host character varying(254),
66
+ last_login_date timestamp without time zone,
67
+ create_date timestamp without time zone DEFAULT now(),
68
+ mod_date timestamp without time zone DEFAULT now(),
69
+ create_by_id integer,
70
+ mod_by_id integer,
71
+ key character varying(32) DEFAULT generate_key(),
72
+ key_sent_date timestamp without time zone,
73
+ CONSTRAINT usr_uniq UNIQUE(usr_name)
74
+ );
75
+ CREATE TABLE usr_grp (
76
+ id serial primary key,
77
+ usr_id integer NOT NULL,
78
+ grp_id integer NOT NULL,
79
+ create_date timestamp without time zone DEFAULT now(),
80
+ mod_date timestamp without time zone DEFAULT now(),
81
+ create_by_id integer,
82
+ mod_by_id integer,
83
+ CONSTRAINT usr_grp_uniq UNIQUE(usr_id, grp_id)
84
+ );
85
+ INSERT INTO usr_grp (usr_id, grp_id, create_by_id, mod_by_id)
86
+ VALUES (1, 1, 1, 1),
87
+ (1, 2, 1, 1);
88
+ COMMIT;
@@ -0,0 +1 @@
1
+ Put locally install libs here.
@@ -0,0 +1 @@
1
+ waxx log files are stored here.
@@ -0,0 +1 @@
1
+ ---
@@ -0,0 +1 @@
1
+ ---
@@ -0,0 +1 @@
1
+ ---
@@ -0,0 +1 @@
1
+ ---
@@ -0,0 +1 @@
1
+ Use this to store files that will be uploaded and downloaded privately (requiring a password or something).