M .gitmodules => .gitmodules +3 -0
@@ 4,3 4,6 @@
[submodule "modules/lume"]
path = modules/lume
url = git@github.com:rxi/lume.git
+[submodule "modules/lua-resty-http"]
+ path = modules/lua-resty-http
+ url = git@github.com:ledgetech/lua-resty-http.git
M Dockerfile => Dockerfile +5 -8
@@ 18,12 18,6 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update \
RUN printf '#!/usr/bin/env bash\nexec rlwrap /app/modules/fennel/fennel "$@"\n' | tee /usr/local/bin/fennel \
&& chmod +x /usr/local/bin/fennel
-# RUN mkdir /var/log/postgres \
-# && echo "deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list \
-# && wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | DEBIAN_FRONTEND=noninteractive apt-key add - \
-# && DEBIAN_FRONTEND=noninteractive apt-get update \
-# && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends postgresql-11
-
ENV NVM_DIR /usr/local/nvm
ENV NODE_VERSION 10.15.1
RUN mkdir $NVM_DIR \
@@ 42,10 36,13 @@ RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | DEBIAN_FRONTEND=noninter
&& DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends yarn
-RUN opm get leafo/pgmoon
+RUN opm get leafo/pgmoon && opm get SkyLothar/lua-resty-jwt
+# openresty doesn't have any program named "lua" by default, but the
+# fennel script uses that as its shebang. luajit works just fine to
+# run fennel.
RUN ln -sf /usr/local/openresty/luajit/bin/luajit /usr/local/bin/lua \
&& make
RUN ln -sf /dev/stdout /app/nginx/logs/access.log \
- && ln -sf /dev/stderr /app/nginx/logs/error.log>
\ No newline at end of file
+ && ln -sf /dev/stderr /app/nginx/logs/error.log
M app/makefile => app/makefile +8 -2
@@ 5,13 5,19 @@ all: dist/app.js dist/app.html
clean:
rm -f dist/*
rm -f src/*.lua
+ rm -f src/components/*.lua
rm -rf node_modules
dist/app.html: src/app.html.fnl
modules/fennel/fennel $< > $@
-dist/app.js: src/react-helpers.lua src/app.lua src/index.js src/set-function-name.js
+dist/app.js: src/react-helpers.lua src/app.lua src/index.js
yarn && yarn run build
-src/app.lua: src/app.fnl src/react-macros.fnl
+COMPONENTS_SRC = $(wildcard src/components/*.fnl)
+COMPONENTS_OBJ = $(patsubst src/components/%.fnl, src/components/%.lua, $(COMPONENTS_SRC))
+src/components/%.lua: src/components/%.fnl
+ modules/fennel/fennel --compile $< > $@
+src/app.lua: src/app.fnl src/react-macros.fnl $(COMPONENTS_OBJ)
modules/fennel/fennel --compile $< > $@
src/react-helpers.lua: src/react-helpers.fnl src/react-macros.fnl
modules/fennel/fennel --compile $< > $@
+
M app/package.json => app/package.json +1 -0
@@ 14,6 14,7 @@
},
"dependencies": {
"fengari-web": "^0.1.4",
+ "jwt-decode": "^2.2.0",
"react": "^16.8.1",
"react-dom": "^16.8.1"
}
M app/src/app.fnl => app/src/app.fnl +2 -33
@@ 2,7 2,6 @@
(local React (require "react"))
(local ReactDOM (require "react-dom"))
(local <> React.Fragment)
-
(local {:use-state use-state
:use-effect use-effect
:use-layout-effect use-layout-effect
@@ 12,33 11,10 @@
:use-context use-context
:get-children-as-array get-children-as-array}
(require "./react-helpers.lua"))
+(local Log (require "./components/Log.lua"))
(local console {:log (fn [...] (: js.global.console :log ...))})
-(component! LogLine [{:children message}]
- (c! :pre {:className :log-message} message))
-
-(component! Log [{:children maybe-children}]
- (let [children (get-children-as-array maybe-children)
- container-ref (use-ref nil)]
- ;; scroll to bottom on change
- (use-effect
- (fn []
- (when (and container-ref.current (not (= container-ref.current js.null))
- container-ref.current.lastChild (not (= container-ref.current.lastChild js.null)))
- (: container-ref.current.lastChild :scrollIntoView (js! {}))))
- (js! [children.length]))
- (c! :div {:style {:color :#EEEEEE
- :backgroundColor :#222222
- :overflowY :scroll
- :position :fixed
- :left :0px
- :right :0px
- :top :100px
- :height "calc(100vh - 200px)"}
- :ref container-ref}
- (: children :map (fn [_ child i] (c! LogLine {:key i} child))))))
-
(component! WebSocketDebugForm
[{:connect connect :disconnect disconnect :on-message on-message}]
(let [input-element (use-ref nil)
@@ 60,13 36,7 @@
:value current-message
:ref input-element
:onChange
- (fn []
- ;; (print (.. "Changing current-message '"
- ;; (if current-message current-message "")
- ;; "' to '"
- ;; input-element.current.value
- ;; "'."))
- (set-current-message input-element.current.value))
+ (fn [] (set-current-message input-element.current.value))
}]
[:button {:type :submit} :Send]]])))
@@ 117,7 87,6 @@
debug-form (c! [WebSocketDebugForm {:connect connect
:disconnect disconnect
:on-message send-message}])]
- (set js.global.fennelLog (fn [_ message] (log message)))
(c! [<> {}
[:div {} (if ws "WebSocket connected" "Websocket not connected")]
debug-form
A app/src/components/Log.fnl => app/src/components/Log.fnl +29 -0
@@ 0,0 1,29 @@
+(require-macros :src.react-macros)
+(local React (require "react"))
+(local {:use-effect use-effect
+ :use-ref use-ref
+ :get-children-as-array get-children-as-array}
+ (require "../react-helpers.lua"))
+(local LogLine (require "./LogLine.lua"))
+
+(component! Log [{:children maybe-children}]
+ (let [children (get-children-as-array maybe-children)
+ container-ref (use-ref nil)]
+ ;; scroll to bottom on change
+ (use-effect
+ (fn []
+ (when (and container-ref.current
+ (not (= container-ref.current js.null))
+ container-ref.current.lastChild
+ (not (= container-ref.current.lastChild js.null)))
+ (: container-ref.current.lastChild :scrollIntoView (js! {}))))
+ (js! [children.length]))
+ (c! :div {:style {:color :#EEEEEE
+ :backgroundColor :#222222
+ :overflowY :scroll
+ :width :100%
+ :height "calc(100vh - 200px)"}
+ :ref container-ref}
+ (: children :map (fn [_ child i] (c! LogLine {:key i} child))))))
+
+Log
A app/src/components/Log.lua => app/src/components/Log.lua +77 -0
@@ 0,0 1,77 @@
+local React = require("react")
+local _0_ = require("../react-helpers.lua")
+local use_ref = _0_["use-ref"]
+local get_children_as_array = _0_["get-children-as-array"]
+local use_effect = _0_["use-effect"]
+local LogLine = require("./LogLine.lua")
+local Log
+do
+ local _6_0
+ local function _9_(_, _, _10_0)
+ local _11_ = _10_0
+ local maybe_children = _11_["children"]
+ do
+ local children = get_children_as_array(maybe_children)
+ local container_ref = use_ref(nil)
+ local function _12_()
+ if (container_ref.current and not (container_ref.current == js.null) and container_ref.current.lastChild and not (container_ref.current.lastChild == js.null)) then
+ local function _14_()
+ local _13_0 = js.new(js.global.Object)
+ return _13_0
+ end
+ return container_ref.current.lastChild:scrollIntoView(_14_())
+ end
+ end
+ local function _14_()
+ local _13_0 = js.new(js.global.Array)
+ _13_0[0] = children.length
+ return _13_0
+ end
+ use_effect(_12_, _14_())
+ do
+ local _15_0
+ do
+ local _17_0 = js.new(js.global.Object)
+ _17_0["ref"] = container_ref
+ local _19_
+ do
+ local _18_0 = js.new(js.global.Object)
+ _18_0["color"] = "#EEEEEE"
+ _18_0["height"] = "calc(100vh - 200px)"
+ _18_0["backgroundColor"] = "#222222"
+ _18_0["overflowY"] = "scroll"
+ _18_0["width"] = "100%"
+ _19_ = _18_0
+ end
+ _17_0["style"] = _19_
+ _15_0 = _17_0
+ end
+ local function _19_(_, child, i)
+ local _20_0
+ do
+ local _22_0 = js.new(js.global.Object)
+ _22_0["key"] = i
+ _20_0 = _22_0
+ end
+ return React:createElement(LogLine, _20_0, child)
+ end
+ return React:createElement("div", _15_0, children:map(_19_))
+ end
+ end
+ end
+ _6_0 = _9_
+ local _1_0 = {displayName = "Log", length = 1, name = "Log"}
+ local _7_0
+ local function _11_(_, _4_0)
+ return rawget(_1_0, _4_0)
+ end
+ _7_0 = _11_
+ local _8_0
+ local function _12_(_, _4_0, _5_0)
+ return rawset(_1_0, _4_0, _5_0)
+ end
+ _8_0 = _12_
+ setmetatable(_1_0, {__call = _6_0, __index = _7_0, __newindex = _8_0})
+ Log = js.createproxy(_1_0, "arrow_function")
+end
+return Log
A app/src/components/LogLine.fnl => app/src/components/LogLine.fnl +7 -0
@@ 0,0 1,7 @@
+(require-macros :src.react-macros)
+(local React (require "react"))
+
+(component! LogLine [{:children message}]
+ (c! :pre {:className :log-message} message))
+
+LogLine
A app/src/components/LogLine.lua => app/src/components/LogLine.lua +33 -0
@@ 0,0 1,33 @@
+local React = require("react")
+local LogLine
+do
+ local _5_0
+ local function _8_(_, _, _9_0)
+ local _10_ = _9_0
+ local message = _10_["children"]
+ do
+ local _11_0
+ do
+ local _13_0 = js.new(js.global.Object)
+ _13_0["className"] = "log-message"
+ _11_0 = _13_0
+ end
+ return React:createElement("pre", _11_0, message)
+ end
+ end
+ _5_0 = _8_
+ local _0_0 = {displayName = "LogLine", length = 1, name = "LogLine"}
+ local _6_0
+ local function _10_(_, _3_0)
+ return rawget(_0_0, _3_0)
+ end
+ _6_0 = _10_
+ local _7_0
+ local function _11_(_, _3_0, _4_0)
+ return rawset(_0_0, _3_0, _4_0)
+ end
+ _7_0 = _11_
+ setmetatable(_0_0, {__call = _5_0, __index = _6_0, __newindex = _7_0})
+ LogLine = js.createproxy(_0_0, "arrow_function")
+end
+return LogLine
A app/src/components/TestComponent.fnl => app/src/components/TestComponent.fnl +4 -0
@@ 0,0 1,4 @@
+(require-macros :src.react-macros)
+
+(component! TestComponent []
+ (c! [:div {} "Hello, world!"]))
A app/src/components/TestComponent.lua => app/src/components/TestComponent.lua +27 -0
@@ 0,0 1,27 @@
+local TestComponent
+do
+ local _5_0
+ local function _8_(_, _)
+ local _9_0
+ do
+ local _11_0 = js.new(js.global.Object)
+ _9_0 = _11_0
+ end
+ return React:createElement("div", _9_0, "Hello, world!")
+ end
+ _5_0 = _8_
+ local _0_0 = {displayName = "TestComponent", length = 0, name = "TestComponent"}
+ local _6_0
+ local function _9_(_, _3_0)
+ return rawget(_0_0, _3_0)
+ end
+ _6_0 = _9_
+ local _7_0
+ local function _10_(_, _3_0, _4_0)
+ return rawset(_0_0, _3_0, _4_0)
+ end
+ _7_0 = _10_
+ setmetatable(_0_0, {__call = _5_0, __index = _6_0, __newindex = _7_0})
+ TestComponent = js.createproxy(_0_0, "arrow_function")
+end
+return nil
M db/.gitignore => db/.gitignore +2 -1
@@ 1,2 1,3 @@
migrations/*.lua
-config.lua>
\ No newline at end of file
+config.lua
+data<
\ No newline at end of file
M db/config.fnl => db/config.fnl +1 -1
@@ 1,4 1,4 @@
-{:host :db
+{:host :172.28.0.102
:port 5432
:database :fennel-openresty
:user :fennel-openresty
M db/migrations/create-users-table.fnl => db/migrations/create-users-table.fnl +0 -2
@@ 14,8 14,6 @@ create table if not exists users (
);
")))
-# hello world
-
(print (.. "creating users table: " (fennelview users-table-created)))
users-table-created
M docker-compose.yml => docker-compose.yml +2 -0
@@ 8,6 8,8 @@ services:
- "8090:80"
volumes:
- .:/app
+ environment:
+ FENNEL_OPENRESTY_JWT_SECRET: Test_Secret!
networks:
nw:
ipv4_address: 172.28.0.101
A modules/lua-resty-http => modules/lua-resty-http +1 -0
@@ 0,0 1,1 @@
+Subproject commit f71e9708a3fd0ff4179d4b0d770c91fcf98d0042
M nginx/conf/nginx.conf => nginx/conf/nginx.conf +1 -0
@@ 1,4 1,5 @@
worker_processes 1;
+env FENNEL_OPENRESTY_JWT_SECRET;
events {
worker_connections 1024;
}
A server/jwt.fnl => server/jwt.fnl +15 -0
@@ 0,0 1,15 @@
+(local jwt (require :resty.jwt))
+(local jwt-secret (os.getenv :FENNEL_OPENRESTY_JWT_SECRET))
+
+(fn get-jwt-token [{:id id :email email}]
+ (: jwt :sign jwt-secret
+ {:header {:typ :JWT :alg :HS512}
+ :payload {:sub id :email email :iat (ngx.time)}}))
+
+(fn verify-jwt-token [token]
+ (let [jwt-obj (: jwt :load_jwt token)
+ verified? (: jwt :verify_jwt_obj jwt-secret jwt-obj)]
+ (when verified? jwt-obj.payload)))
+
+{:get-token get-jwt-token
+ :verify-token verify-jwt-token}
M server/server.fnl => server/server.fnl +67 -2
@@ 1,8 1,73 @@
(global ngx ngx)
(local pgmoon (require :pgmoon))
-(local html (require :server.html))
+(local cjson (require :cjson))
+(local html (fn [...] (.. "<!doctype html>" ((require :server.html) ...))))
(global _G (. (getmetatable _G) :__index))
(local fennel (require :modules.fennel.fennel))
(local fennelview (require :modules.fennel.fennelview))
+(local lume (require :modules.lume.lume))
+(local jwt (require :server.jwt))
-(ngx.say (.. "<!doctype html>\n" (html [:div {} :WTF])))
+(local pg (pgmoon.new (require :db.config)))
+(local pg-status (assert (: pg :connect)))
+
+(local (h h-err) (ngx.req.get_headers))
+(local method (ngx.req.get_method))
+
+(fn say-serialized [tab]
+ (if (and h (= h.Accept "text/x-fennelview"))
+ (do (set ngx.header.Content-Type "text/x-fennelview")
+ (ngx.say (fennelview tab {:indent ""})))
+ (do (set ngx.header.Content-Type "application/json")
+ (ngx.say (cjson.encode tab)))))
+
+(fn index-route []
+ (say-serialized {:authenticate :/api/authenticate}))
+
+(fn unknown-route []
+ (let [message (.. "Error 404 - unknown route " ngx.var.uri " for method " method)
+ response {:error {:message message :code 404}}]
+ (set ngx.status ngx.HTTP_NOT_FOUND)
+ (say-serialized response)))
+
+;; returns a user id if successful
+(fn authenticate-user-by-email [{:email email :password password}]
+ (let [esc_email (: pg :escape_literal email)
+ esc_password (: pg :escape_literal password)
+ query (.. "select id, email from users where"
+ " email=lower(" esc_email ") and"
+ " password=crypt(" esc_password ", password)"
+ " limit 1;")
+ result (: pg :query query)]
+ (when (and result (. result 1)) (. result 1))))
+
+(local authenticate-wrong-method-error-message
+ (.. "Method not allowed - use a POST with a JSON body including"
+ " 'email' and 'password' to log in and receive a JWT which"
+ " can be used as a bearer token to authenticate with the rest"
+ " of the API."))
+
+(fn authenticate-route []
+ (match method
+ :GET (do (set ngx.status ngx.HTTP_NOT_ALLOWED)
+ (say-serialized
+ {:error {:message authenticate-wrong-method-error-message
+ :code 405}}))
+ :POST (do (ngx.req.read_body)
+ (let [raw-body (ngx.req.get_body_data)
+ body (if raw-body (cjson.decode raw-body) {})]
+ (if (and body.email body.password)
+ (let [user (authenticate-user-by-email body)]
+ (say-serialized (jwt.get-token user)))
+ (do (set ngx.status ngx.BAD_REQUEST)
+ (say-serialized {:error {:message "Bad request" :code 400}})))))
+ _ (unknown-route)))
+
+((fn router [uri]
+ (let [path (lume.slice (lume.split uri :/) 2)
+ route (match path
+ [:api :authenticate nil] authenticate-route
+ [:api nil] index-route
+ _ unknown-route)]
+ (route)))
+ ngx.var.uri)
M server/wss.fnl => server/wss.fnl +2 -8
@@ 1,15 1,9 @@
(global ngx ngx)
(local server (require :resty.websocket.server))
-
(local pgmoon (require :pgmoon))
+(local jwt (require :server.jwt))
-(local pg
- (pgmoon.new
- {:host :172.28.0.102
- :port :5432
- :database :fennel-openresty
- :user :fennel-openresty
- :password "go fennel!"}))
+(local pg (pgmoon.new (require :db.config)))
(local (pg-status pg-err) (: pg :connect))
((fn [] (when (not pg-status)
(ngx.log ngx.ERR (.. "error connecting to db: " (tostring pg-err))))))