tep 0.11.0

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 (193) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Makefile +134 -0
  4. data/README.md +247 -0
  5. data/SINATRA_COMPAT.md +376 -0
  6. data/bin/tep +2156 -0
  7. data/examples/agentic_chat/README.md +103 -0
  8. data/examples/agentic_chat/app.rb +310 -0
  9. data/examples/api_gateway/README.md +49 -0
  10. data/examples/api_gateway/app.rb +66 -0
  11. data/examples/blog/app.rb +367 -0
  12. data/examples/blog/views/index.erb +36 -0
  13. data/examples/blog/views/login.erb +28 -0
  14. data/examples/blog/views/new_post.erb +25 -0
  15. data/examples/blog/views/show.erb +16 -0
  16. data/examples/chat/app.rb +278 -0
  17. data/examples/chat/assets/logo.svg +13 -0
  18. data/examples/chat/assets/style.css +209 -0
  19. data/examples/chat/views/index.erb +142 -0
  20. data/examples/chatbot/README.md +111 -0
  21. data/examples/chatbot/app.rb +1024 -0
  22. data/examples/chatbot/assets/chat.js +249 -0
  23. data/examples/chatbot/assets/compare.js +93 -0
  24. data/examples/chatbot/assets/markdown.js +84 -0
  25. data/examples/chatbot/assets/style.css +215 -0
  26. data/examples/chatbot/schema.sql +25 -0
  27. data/examples/chatbot/views/compare.erb +43 -0
  28. data/examples/chatbot/views/index.erb +42 -0
  29. data/examples/chatbot/views/login.erb +22 -0
  30. data/examples/chatbot/views/setup.erb +23 -0
  31. data/examples/counter/README.md +68 -0
  32. data/examples/counter/app.rb +85 -0
  33. data/examples/experiments/AGENTS.md +91 -0
  34. data/examples/experiments/README.md +99 -0
  35. data/examples/experiments/app.rb +225 -0
  36. data/examples/geohash/Gemfile +11 -0
  37. data/examples/geohash/Gemfile.lock +17 -0
  38. data/examples/geohash/README.md +58 -0
  39. data/examples/geohash/app.rb +33 -0
  40. data/examples/hello.rb +120 -0
  41. data/examples/llm_gateway/README.md +73 -0
  42. data/examples/llm_gateway/app.rb +91 -0
  43. data/examples/maidenhead/Gemfile +7 -0
  44. data/examples/maidenhead/Gemfile.lock +17 -0
  45. data/examples/maidenhead/README.md +47 -0
  46. data/examples/maidenhead/app.rb +46 -0
  47. data/examples/pg_hello.rb +76 -0
  48. data/examples/qdrant/Gemfile +11 -0
  49. data/examples/qdrant/Gemfile.lock +29 -0
  50. data/examples/qdrant/README.md +54 -0
  51. data/examples/sinatra_style.rb +32 -0
  52. data/examples/websocket_echo.rb +37 -0
  53. data/lib/tep/agent_delegation.rb +35 -0
  54. data/lib/tep/app.rb +291 -0
  55. data/lib/tep/assets.rb +52 -0
  56. data/lib/tep/auth.rb +78 -0
  57. data/lib/tep/auth_bearer_token.rb +126 -0
  58. data/lib/tep/auth_oauth2.rb +189 -0
  59. data/lib/tep/auth_oauth2_client.rb +29 -0
  60. data/lib/tep/auth_oauth2_code.rb +40 -0
  61. data/lib/tep/auth_session_cookie.rb +132 -0
  62. data/lib/tep/broadcast.rb +265 -0
  63. data/lib/tep/broadcast_subscription.rb +42 -0
  64. data/lib/tep/cache.rb +49 -0
  65. data/lib/tep/events.rb +257 -0
  66. data/lib/tep/filter.rb +21 -0
  67. data/lib/tep/handler.rb +35 -0
  68. data/lib/tep/http.rb +599 -0
  69. data/lib/tep/identity.rb +67 -0
  70. data/lib/tep/job.rb +186 -0
  71. data/lib/tep/json.rb +572 -0
  72. data/lib/tep/jwt.rb +126 -0
  73. data/lib/tep/live_view.rb +219 -0
  74. data/lib/tep/llm.rb +505 -0
  75. data/lib/tep/logger.rb +85 -0
  76. data/lib/tep/mcp.rb +203 -0
  77. data/lib/tep/multipart.rb +98 -0
  78. data/lib/tep/net.rb +155 -0
  79. data/lib/tep/openai_server.rb +725 -0
  80. data/lib/tep/parallel.rb +168 -0
  81. data/lib/tep/parser.rb +81 -0
  82. data/lib/tep/password.rb +102 -0
  83. data/lib/tep/pg.rb +1128 -0
  84. data/lib/tep/presence.rb +589 -0
  85. data/lib/tep/presence_entry.rb +52 -0
  86. data/lib/tep/proxy.rb +801 -0
  87. data/lib/tep/request.rb +194 -0
  88. data/lib/tep/response.rb +134 -0
  89. data/lib/tep/router.rb +137 -0
  90. data/lib/tep/scheduler.rb +342 -0
  91. data/lib/tep/security.rb +140 -0
  92. data/lib/tep/server.rb +276 -0
  93. data/lib/tep/server_scheduled.rb +375 -0
  94. data/lib/tep/session.rb +98 -0
  95. data/lib/tep/shell.rb +62 -0
  96. data/lib/tep/sphttp.c +858 -0
  97. data/lib/tep/sqlite.rb +215 -0
  98. data/lib/tep/streamer.rb +31 -0
  99. data/lib/tep/tep_pg.c +769 -0
  100. data/lib/tep/tep_sqlite.c +320 -0
  101. data/lib/tep/url.rb +161 -0
  102. data/lib/tep/version.rb +3 -0
  103. data/lib/tep/websocket/connection.rb +171 -0
  104. data/lib/tep/websocket/driver.rb +169 -0
  105. data/lib/tep/websocket/frame.rb +238 -0
  106. data/lib/tep/websocket/handshake.rb +159 -0
  107. data/lib/tep/websocket.rb +68 -0
  108. data/lib/tep.rb +981 -0
  109. data/public/hello.txt +1 -0
  110. data/public/style.css +4 -0
  111. data/spinel-ext.json +33 -0
  112. data/test/helper.rb +248 -0
  113. data/test/real_world/01_simple.rb +5 -0
  114. data/test/real_world/02_lifecycle.rb +20 -0
  115. data/test/real_world/03_chat.rb +75 -0
  116. data/test/real_world/04_health_api.rb +25 -0
  117. data/test/real_world/05_todo_api.rb +57 -0
  118. data/test/real_world/06_basic_auth.rb +25 -0
  119. data/test/real_world/07_bbc_rest_api.rb +228 -0
  120. data/test/real_world/07_sklise_things.rb +109 -0
  121. data/test/real_world/08_jwd83_helloworld.rb +56 -0
  122. data/test/run_all.rb +7 -0
  123. data/test/run_parallel.rb +89 -0
  124. data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
  125. data/test/test_api_gateway.rb +76 -0
  126. data/test/test_auth.rb +223 -0
  127. data/test/test_auth_oauth2.rb +208 -0
  128. data/test/test_auth_session_cookie.rb +198 -0
  129. data/test/test_broadcast.rb +197 -0
  130. data/test/test_broadcast_pg.rb +135 -0
  131. data/test/test_cache.rb +98 -0
  132. data/test/test_cache_static.rb +48 -0
  133. data/test/test_cookies.rb +52 -0
  134. data/test/test_erb.rb +53 -0
  135. data/test/test_erb_ivars.rb +58 -0
  136. data/test/test_events.rb +114 -0
  137. data/test/test_filters.rb +41 -0
  138. data/test/test_geohash_example.rb +89 -0
  139. data/test/test_http.rb +137 -0
  140. data/test/test_http_pool.rb +122 -0
  141. data/test/test_http_pool_send.rb +57 -0
  142. data/test/test_identity.rb +165 -0
  143. data/test/test_inbound_tls.rb +101 -0
  144. data/test/test_inbound_tls_scheduled.rb +101 -0
  145. data/test/test_job.rb +108 -0
  146. data/test/test_json.rb +168 -0
  147. data/test/test_jwt.rb +143 -0
  148. data/test/test_live_view.rb +324 -0
  149. data/test/test_llm.rb +250 -0
  150. data/test/test_llm_gateway.rb +95 -0
  151. data/test/test_logger.rb +101 -0
  152. data/test/test_maidenhead_example.rb +86 -0
  153. data/test/test_mcp.rb +264 -0
  154. data/test/test_misc_v02.rb +54 -0
  155. data/test/test_modular.rb +43 -0
  156. data/test/test_multi_filters.rb +40 -0
  157. data/test/test_mustache.rb +57 -0
  158. data/test/test_openai_server.rb +598 -0
  159. data/test/test_optional_segments.rb +45 -0
  160. data/test/test_parallel.rb +102 -0
  161. data/test/test_params.rb +99 -0
  162. data/test/test_pass.rb +42 -0
  163. data/test/test_password.rb +101 -0
  164. data/test/test_pg.rb +673 -0
  165. data/test/test_presence.rb +374 -0
  166. data/test/test_presence_pg.rb +309 -0
  167. data/test/test_proxy.rb +556 -0
  168. data/test/test_proxy_dsl.rb +119 -0
  169. data/test/test_proxy_streaming.rb +146 -0
  170. data/test/test_real_world.rb +397 -0
  171. data/test/test_regex_routes.rb +52 -0
  172. data/test/test_request_methods.rb +102 -0
  173. data/test/test_response.rb +123 -0
  174. data/test/test_routing.rb +109 -0
  175. data/test/test_scheduler.rb +153 -0
  176. data/test/test_security.rb +72 -0
  177. data/test/test_server_scheduled.rb +56 -0
  178. data/test/test_sessions.rb +59 -0
  179. data/test/test_shell.rb +54 -0
  180. data/test/test_sqlite.rb +148 -0
  181. data/test/test_sqlite_cached.rb +171 -0
  182. data/test/test_static.rb +57 -0
  183. data/test/test_streaming.rb +96 -0
  184. data/test/test_unsupported.rb +32 -0
  185. data/test/test_websocket.rb +152 -0
  186. data/test/test_websocket_echo.rb +138 -0
  187. data/test/views/greet.erb +5 -0
  188. data/test/views/hello.erb +5 -0
  189. data/test/views/list.erb +5 -0
  190. data/test/views/m_ivars.mustache +3 -0
  191. data/test/views/m_simple.mustache +4 -0
  192. data/test/views/mixed.erb +3 -0
  193. metadata +264 -0
@@ -0,0 +1,367 @@
1
+ # tep blog -- a flagship demo exercising every batteries-included
2
+ # tep feature in a coherent ~200 lines:
3
+ #
4
+ # - Tep::SQLite posts + users tables
5
+ # - Tep::Password PBKDF2 password hashing
6
+ # - Tep::Jwt JSON API token issue / verify
7
+ # - Sessions web-side login (signed cookie)
8
+ # - Tep::Json JSON encode + flat-key decode
9
+ # - Tep::Logger request log + auth events
10
+ # - Tep::Security CORS + secure-headers
11
+ # - ERB + @ivar locals public-facing views
12
+ #
13
+ # Build + run:
14
+ #
15
+ # bin/tep build examples/blog/app.rb -o /tmp/blog
16
+ # TEP_SESSION_SECRET=$(openssl rand -hex 32) /tmp/blog -p 4567
17
+ #
18
+ # First-time setup creates /tmp/tep_blog.db and seeds an admin
19
+ # user (alice / hunter2). See SINATRA_COMPAT.md for the feature
20
+ # matrix this app exercises end-to-end.
21
+
22
+ require 'sinatra'
23
+
24
+ # -------------------------------------------------------------------
25
+ # Configuration
26
+ # -------------------------------------------------------------------
27
+
28
+ DB_PATH = ENV.fetch("TEP_BLOG_DB", "/tmp/tep_blog.db")
29
+ JWT_SECRET = ENV.fetch("TEP_JWT_SECRET", "dev-jwt-secret-change-me")
30
+ SESSION_SEED = ENV.fetch("TEP_SESSION_SECRET", "dev-session-secret-change-me")
31
+ SEED_USER = "alice"
32
+ SEED_PASSWORD = "hunter2"
33
+
34
+ # Sessions need a stable HMAC secret; in production set
35
+ # TEP_SESSION_SECRET to 32 random bytes. We accept the dev default
36
+ # at build time for convenience.
37
+ Tep.session_secret = SESSION_SEED
38
+
39
+ LOGGER = Tep::Logger.new
40
+ LOGGER.set_level("info")
41
+
42
+ CORS = Tep::Security::Cors.new
43
+ CORS.set_origin("*")
44
+ CORS.set_allowed_verbs("GET,POST,OPTIONS")
45
+ CORS.set_allowed_headers("Content-Type,Authorization")
46
+ Tep.before CORS
47
+
48
+ HEADERS = Tep::Security::Headers.new
49
+ Tep.after HEADERS
50
+
51
+ set :views, File.expand_path("views", __dir__)
52
+
53
+ # -------------------------------------------------------------------
54
+ # Schema + seed
55
+ # -------------------------------------------------------------------
56
+
57
+ on_start do
58
+ db = Tep::SQLite.new
59
+ if db.open(DB_PATH)
60
+ db.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT UNIQUE, pwd_hash TEXT)")
61
+ db.exec("CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, title TEXT, body TEXT, author TEXT, created_at INTEGER)")
62
+ # Seed the admin user once if the table is empty.
63
+ n = db.first_int("SELECT count(*) FROM users", "")
64
+ if n == 0
65
+ hash = Tep::Password.hash(SEED_PASSWORD)
66
+ db.prepare("INSERT INTO users (name, pwd_hash) VALUES (?, ?)")
67
+ db.bind_str(1, SEED_USER)
68
+ db.bind_str(2, hash)
69
+ db.step
70
+ db.finalize
71
+ LOGGER.info("seeded admin user: " + SEED_USER)
72
+ end
73
+ # Seed an introductory post on the first boot so the homepage
74
+ # isn't empty for a new install. Idempotent: only inserts when
75
+ # the posts table is still empty, so wiping `users` on its own
76
+ # won't double-seed and re-seeding never duplicates.
77
+ pn = db.first_int("SELECT count(*) FROM posts", "")
78
+ if pn == 0
79
+ seed_body =
80
+ "<p>This blog is the flagship demo for " +
81
+ "<a href=\"https://github.com/OriPekelman/tep\">tep</a>, " +
82
+ "a Sinatra-flavoured framework that compiles to a single " +
83
+ "static binary via <a href=\"https://github.com/matz/spinel\">Spinel</a> " +
84
+ "(an AOT Ruby compiler).</p>" +
85
+ "<p>The whole site -- routes, ERB views, sessions, JSON " +
86
+ "API, JWT-authed writes, and the SQLite store you're " +
87
+ "reading from -- ships in <code>examples/blog/app.rb</code> " +
88
+ "(~250 lines) plus four ERB templates. No Rack, no Bundler, " +
89
+ "no MRI runtime: <code>tep build</code> turns it into a " +
90
+ "C-compiled binary that links libsqlite3 and serves HTTP " +
91
+ "directly via a small <code>sphttp.c</code> shim.</p>" +
92
+ "<p>Browse around: log in as <code>alice / hunter2</code> " +
93
+ "to write a post, or hit <code>GET /api/posts</code> for " +
94
+ "the JSON view. <code>POST /api/token</code> issues a JWT " +
95
+ "you can use against <code>POST /api/posts</code>.</p>"
96
+ db.prepare("INSERT INTO posts (title, body, author, created_at) VALUES (?, ?, ?, ?)")
97
+ db.bind_str(1, "Welcome to tep + spinel")
98
+ db.bind_str(2, seed_body)
99
+ db.bind_str(3, SEED_USER)
100
+ db.bind_int(4, Time.now.to_i)
101
+ db.step
102
+ db.finalize
103
+ LOGGER.info("seeded intro post")
104
+ end
105
+ db.close
106
+ end
107
+ end
108
+
109
+ # -------------------------------------------------------------------
110
+ # Per-request log
111
+ # -------------------------------------------------------------------
112
+
113
+ before do
114
+ LOGGER.info(req.verb + " " + req.path)
115
+ end
116
+
117
+ # -------------------------------------------------------------------
118
+ # Helpers (inlined per route -- spinel's translator doesn't do
119
+ # `helpers do ... end` blocks, by design)
120
+ # -------------------------------------------------------------------
121
+ #
122
+ # db_open() -> Tep::SQLite already open on DB_PATH
123
+ # current_user(req) -> session-cookie name or "" when absent
124
+ # require_login(req, res) -> set 401 + halt if not logged in
125
+ # verify_jwt_user(req) -> the `sub` claim from the bearer token, or "" on failure
126
+ #
127
+ # Callers use simple if-checks; no closures.
128
+
129
+ # -------------------------------------------------------------------
130
+ # Public web pages
131
+ # -------------------------------------------------------------------
132
+
133
+ get '/' do
134
+ db = Tep::SQLite.new
135
+ db.open(DB_PATH)
136
+
137
+ posts_html = ""
138
+ db.prepare("SELECT id, title, author, created_at FROM posts ORDER BY id DESC")
139
+ while db.step == 1
140
+ posts_html = posts_html +
141
+ "<li><a href=\"/post/" + db.col_int(0).to_s + "\">" +
142
+ Tep.h(db.col_str(1)) + "</a> <span>by " +
143
+ Tep.h(db.col_str(2)) + "</span></li>"
144
+ end
145
+ db.finalize
146
+ db.close
147
+
148
+ @posts_html = posts_html
149
+ @logged_in = req.session.has?("user") ? "1" : ""
150
+ @user = req.session.get("user")
151
+ erb :index
152
+ end
153
+
154
+ get '/post/:id' do
155
+ db = Tep::SQLite.new
156
+ db.open(DB_PATH)
157
+ id = params[:id]
158
+ @title = db.first_str("SELECT title FROM posts WHERE id = ?", id)
159
+ @body = db.first_str("SELECT body FROM posts WHERE id = ?", id)
160
+ @author = db.first_str("SELECT author FROM posts WHERE id = ?", id)
161
+ db.close
162
+
163
+ if @title.length == 0
164
+ res.set_status(404)
165
+ return "<h1>not found</h1>"
166
+ end
167
+ erb :show
168
+ end
169
+
170
+ # -------------------------------------------------------------------
171
+ # Auth (web): sessions
172
+ # -------------------------------------------------------------------
173
+
174
+ get '/login' do
175
+ @flash = ""
176
+ erb :login
177
+ end
178
+
179
+ post '/login' do
180
+ user = params[:user]
181
+ pwd = params[:password]
182
+
183
+ db = Tep::SQLite.new
184
+ db.open(DB_PATH)
185
+ db.prepare("SELECT pwd_hash FROM users WHERE name = ?")
186
+ db.bind_str(1, user)
187
+ hash = ""
188
+ if db.step == 1
189
+ hash = db.col_str(0)
190
+ end
191
+ db.finalize
192
+ db.close
193
+
194
+ if hash.length > 0 && Tep::Password.verify(pwd, hash)
195
+ req.session.set("user", user)
196
+ LOGGER.info("login ok: " + user)
197
+ res.headers["Location"] = "/"
198
+ res.set_status(302)
199
+ return ""
200
+ end
201
+
202
+ LOGGER.warn("login failed: " + user)
203
+ @flash = "invalid credentials"
204
+ res.set_status(401)
205
+ erb :login
206
+ end
207
+
208
+ post '/logout' do
209
+ user = req.session.get("user")
210
+ req.session.set("user", "")
211
+ LOGGER.info("logout: " + user)
212
+ res.headers["Location"] = "/"
213
+ res.set_status(302)
214
+ ""
215
+ end
216
+
217
+ # -------------------------------------------------------------------
218
+ # Admin (session-required) -- create posts
219
+ # -------------------------------------------------------------------
220
+
221
+ get '/admin/new' do
222
+ if !req.session.has?("user")
223
+ res.set_status(401)
224
+ return "<h1>401</h1><p><a href=\"/login\">log in</a></p>"
225
+ end
226
+ @user = req.session.get("user")
227
+ erb :new_post
228
+ end
229
+
230
+ post '/admin/new' do
231
+ if !req.session.has?("user")
232
+ res.set_status(401)
233
+ return ""
234
+ end
235
+ user = req.session.get("user")
236
+
237
+ db = Tep::SQLite.new
238
+ db.open(DB_PATH)
239
+ db.prepare("INSERT INTO posts (title, body, author, created_at) VALUES (?, ?, ?, ?)")
240
+ db.bind_str(1, params[:title])
241
+ db.bind_str(2, params[:body])
242
+ db.bind_str(3, user)
243
+ db.bind_int(4, Time.now.to_i)
244
+ db.step
245
+ db.finalize
246
+ id = db.last_rowid
247
+ db.close
248
+
249
+ LOGGER.info("post created id=" + id.to_s + " by " + user)
250
+ res.headers["Location"] = "/post/" + id.to_s
251
+ res.set_status(302)
252
+ ""
253
+ end
254
+
255
+ # -------------------------------------------------------------------
256
+ # JSON API
257
+ # -------------------------------------------------------------------
258
+
259
+ get '/api/posts' do
260
+ res.headers["Content-Type"] = "application/json"
261
+ db = Tep::SQLite.new
262
+ db.open(DB_PATH)
263
+
264
+ out = "["
265
+ first = true
266
+ db.prepare("SELECT id, title, author FROM posts ORDER BY id DESC")
267
+ while db.step == 1
268
+ if !first
269
+ out = out + ","
270
+ end
271
+ first = false
272
+ out = out + "{" +
273
+ Tep::Json.encode_pair_int("id", db.col_int(0)) + "," +
274
+ Tep::Json.encode_pair_str("title", db.col_str(1)) + "," +
275
+ Tep::Json.encode_pair_str("author", db.col_str(2)) + "}"
276
+ end
277
+ db.finalize
278
+ db.close
279
+ out + "]"
280
+ end
281
+
282
+ get '/api/posts/:id' do
283
+ res.headers["Content-Type"] = "application/json"
284
+ db = Tep::SQLite.new
285
+ db.open(DB_PATH)
286
+ id = params[:id]
287
+ title = db.first_str("SELECT title FROM posts WHERE id = ?", id)
288
+ body = db.first_str("SELECT body FROM posts WHERE id = ?", id)
289
+ author = db.first_str("SELECT author FROM posts WHERE id = ?", id)
290
+ db.close
291
+ if title.length == 0
292
+ res.set_status(404)
293
+ return "{}"
294
+ end
295
+ "{" +
296
+ Tep::Json.encode_pair_str("title", title) + "," +
297
+ Tep::Json.encode_pair_str("body", body) + "," +
298
+ Tep::Json.encode_pair_str("author", author) + "}"
299
+ end
300
+
301
+ # Issue a JWT for API access. Same credentials as web login.
302
+ post '/api/token' do
303
+ res.headers["Content-Type"] = "application/json"
304
+ user = Tep::Json.get_str(req.raw_body, "user")
305
+ pwd = Tep::Json.get_str(req.raw_body, "password")
306
+
307
+ db = Tep::SQLite.new
308
+ db.open(DB_PATH)
309
+ db.prepare("SELECT pwd_hash FROM users WHERE name = ?")
310
+ db.bind_str(1, user)
311
+ hash = ""
312
+ if db.step == 1
313
+ hash = db.col_str(0)
314
+ end
315
+ db.finalize
316
+ db.close
317
+
318
+ if hash.length == 0 || !Tep::Password.verify(pwd, hash)
319
+ res.set_status(401)
320
+ LOGGER.warn("api token denied: " + user)
321
+ return "{\"error\":\"invalid credentials\"}"
322
+ end
323
+
324
+ payload = "{" +
325
+ Tep::Json.encode_pair_str("sub", user) + "," +
326
+ Tep::Json.encode_pair_int("exp", Time.now.to_i + 3600) + "}"
327
+ token = Tep::Jwt.encode_hs256(payload, JWT_SECRET)
328
+ LOGGER.info("api token issued: " + user)
329
+ "{\"token\":\"" + token + "\"}"
330
+ end
331
+
332
+ post '/api/posts' do
333
+ res.headers["Content-Type"] = "application/json"
334
+ auth = req.req_headers["authorization"]
335
+ bearer = ""
336
+ if auth.length > 7 && auth[0, 7] == "Bearer "
337
+ bearer = auth[7, auth.length - 7]
338
+ end
339
+ payload = ""
340
+ if bearer.length > 0
341
+ payload = Tep::Jwt.verify_and_decode(bearer, JWT_SECRET)
342
+ end
343
+ if payload.length == 0
344
+ res.set_status(401)
345
+ return "{\"error\":\"unauthorized\"}"
346
+ end
347
+ user = Tep::Json.get_str(payload, "sub")
348
+
349
+ title = Tep::Json.get_str(req.raw_body, "title")
350
+ body = Tep::Json.get_str(req.raw_body, "body")
351
+
352
+ db = Tep::SQLite.new
353
+ db.open(DB_PATH)
354
+ db.prepare("INSERT INTO posts (title, body, author, created_at) VALUES (?, ?, ?, ?)")
355
+ db.bind_str(1, title)
356
+ db.bind_str(2, body)
357
+ db.bind_str(3, user)
358
+ db.bind_int(4, Time.now.to_i)
359
+ db.step
360
+ db.finalize
361
+ id = db.last_rowid
362
+ db.close
363
+
364
+ LOGGER.info("api post created id=" + id.to_s + " by " + user)
365
+ res.set_status(201)
366
+ "{" + Tep::Json.encode_pair_int("id", id) + "}"
367
+ end
@@ -0,0 +1,36 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>tep blog</title>
6
+ <style>
7
+ body { font: 16px/1.5 -apple-system, sans-serif; max-width: 720px; margin: 2em auto; padding: 0 1em; color: #222; }
8
+ nav { display: flex; justify-content: space-between; padding-bottom: 1em; border-bottom: 1px solid #ddd; }
9
+ li { margin: .5em 0; }
10
+ li span { color: #888; font-size: .9em; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <nav>
15
+ <h1 style="margin:0">tep blog</h1>
16
+ <div>
17
+ <% if @logged_in == "1" %>
18
+ signed in as <strong><%= @user %></strong>
19
+ &middot; <a href="/admin/new">new post</a>
20
+ &middot; <form method="post" action="/logout" style="display:inline"><button>log out</button></form>
21
+ <% else %>
22
+ <a href="/login">log in</a>
23
+ <% end %>
24
+ </div>
25
+ </nav>
26
+
27
+ <h2>posts</h2>
28
+ <ul>
29
+ <%= @posts_html %>
30
+ </ul>
31
+
32
+ <% if @posts_html.length == 0 %>
33
+ <p>no posts yet. <a href="/login">log in</a> as <code>alice / hunter2</code> to write one.</p>
34
+ <% end %>
35
+ </body>
36
+ </html>
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>tep blog -- log in</title>
6
+ <style>
7
+ body { font: 16px/1.5 -apple-system, sans-serif; max-width: 360px; margin: 4em auto; padding: 0 1em; }
8
+ label { display: block; margin: 1em 0 .25em; }
9
+ input { width: 100%; padding: .5em; box-sizing: border-box; }
10
+ button { margin-top: 1em; padding: .6em 1em; }
11
+ .flash { color: #b00; }
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <h1>log in</h1>
16
+ <% if @flash.length > 0 %>
17
+ <p class="flash"><%= @flash %></p>
18
+ <% end %>
19
+ <form method="post" action="/login">
20
+ <label>username</label>
21
+ <input name="user" autofocus>
22
+ <label>password</label>
23
+ <input name="password" type="password">
24
+ <button>log in</button>
25
+ </form>
26
+ <p style="margin-top:2em;color:#888">demo user: <code>alice / hunter2</code></p>
27
+ </body>
28
+ </html>
@@ -0,0 +1,25 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>tep blog -- new post</title>
6
+ <style>
7
+ body { font: 16px/1.5 -apple-system, sans-serif; max-width: 720px; margin: 2em auto; padding: 0 1em; }
8
+ label { display: block; margin: 1em 0 .25em; }
9
+ input, textarea { width: 100%; padding: .5em; box-sizing: border-box; font: inherit; }
10
+ textarea { min-height: 200px; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <p><a href="/">&larr; all posts</a></p>
15
+ <h1>new post</h1>
16
+ <p style="color:#888">posting as <strong><%= @user %></strong></p>
17
+ <form method="post" action="/admin/new">
18
+ <label>title</label>
19
+ <input name="title">
20
+ <label>body (HTML allowed)</label>
21
+ <textarea name="body"></textarea>
22
+ <button>publish</button>
23
+ </form>
24
+ </body>
25
+ </html>
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title><%= @title %></title>
6
+ <style>body { font: 16px/1.5 -apple-system, sans-serif; max-width: 720px; margin: 2em auto; padding: 0 1em; }</style>
7
+ </head>
8
+ <body>
9
+ <p><a href="/">&larr; all posts</a></p>
10
+ <article>
11
+ <h1><%= @title %></h1>
12
+ <p style="color:#888">by <%= @author %></p>
13
+ <div><%= @body %></div>
14
+ </article>
15
+ </body>
16
+ </html>