Mercurial Hosting > lang
changeset 2:78708fa556a0
add login
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Tue, 08 Jul 2025 15:55:34 -0600 |
parents | 1c87f785eb42 |
children | eee6d4f59811 |
files | .hgignore src/chat.html.luan src/do_login.html.luan src/index.html.luan src/lib/Db.luan src/lib/Shared.luan src/lib/User.luan src/lib/Utils.luan src/lib/ai/Ai.luan src/login.html.luan src/login.js.luan src/login_sent.html.luan src/private/Config_sample.luan src/site.css src/site.js |
diffstat | 15 files changed, 456 insertions(+), 17 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgignore Tue Jul 08 14:18:25 2025 -0600 +++ b/.hgignore Tue Jul 08 15:55:34 2025 -0600 @@ -1,3 +1,5 @@ syntax: glob err +local/ +private/Config.luan
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/chat.html.luan Tue Jul 08 15:55:34 2025 -0600 @@ -0,0 +1,46 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local Io = require "luan:Io.luan" +local Http = require "luan:http/Http.luan" +local Shared = require "site:/lib/Shared.luan" +local head = Shared.head or error() +local header = Shared.header or error() + + +local function get_ai_thread(ai_key) + return "thread" +end + +return function() + local ai_key = "whatever" + local thread = get_ai_thread(ai_key) + Io.stdout = Http.response.text_writer() +%> +<!doctype html> +<html lang="en"> + <head> +<% head() %> + </head> + <body> +<% header() %> + <div content> + <h1>Chat</h1> + <div ai_container="<%=ai_key%>" > + <div flex> + <div scroll> + <h2>Let's chat</h2> + <div messages> + </div> + </div> + <div ask> + <textarea autofocus oninput="fixTextarea(event)" onkeydown="textareaKey('<%=ai_key%>',event)"></textarea> + <button onclick="askAi('<%=ai_key%>')" title="Send"><img src="/images/send.svg"></button> + </div> + </div> + <img waiting-ai-icon src="/images/spinner_green.gif"> + </div> + </div> + </body> +</html> +<% +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/do_login.html.luan Tue Jul 08 15:55:34 2025 -0600 @@ -0,0 +1,40 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local Io = require "luan:Io.luan" +local Http = require "luan:http/Http.luan" +local User = require "site:/lib/User.luan" +local Shared = require "site:/lib/Shared.luan" +local head = Shared.head or error() +local header = Shared.header or error() + + +return function() + local user_id = Http.request.parameters.user or error() + local password = Http.request.parameters.password or error() + local user = User.get_by_id(user_id) + Io.stdout = Http.response.text_writer() +%> +<!doctype html> +<html lang="en"> + <head> +<% head() %> + </head> + <body> +<% header() %> + <div content> + <h1>Login / Register</h1> +<% if user == nil or user.password ~= password then %> + <p>Login failed</p> +<% + else + user.login() +%> + <script> location = '/'; </script> +<% + end +%> + </div> + </body> +</html> +<% +end
--- a/src/index.html.luan Tue Jul 08 14:18:25 2025 -0600 +++ b/src/index.html.luan Tue Jul 08 15:55:34 2025 -0600 @@ -8,34 +8,18 @@ return function() - local ai_key = "whatever" Io.stdout = Http.response.text_writer() %> <!doctype html> <html lang="en"> <head> <% head() %> - <title>Lang</title> - <style> - </style> </head> <body> <% header() %> <div content> <h1>Lang</h1> - <div ai_container="<%=ai_key%>" > - <div flex> - <div scroll> - <h2>Let's chat</h2> - <div messages></div> - </div> - <div ask> - <textarea autofocus oninput="fixTextarea(event)" onkeydown="textareaKey('<%=ai_key%>',event)"></textarea> - <button onclick="askAi('<%=ai_key%>')" title="Send"><img src="/images/send.svg"></button> - </div> - </div> - <img waiting-ai-icon src="/images/spinner_green.gif"> - </div> + <p><a href="chat.html">chat</a></p> </div> </body> </html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/Db.luan Tue Jul 08 15:55:34 2025 -0600 @@ -0,0 +1,27 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local Io = require "luan:Io.luan" +local uri = Io.uri or error() +local Thread = require "luan:Thread.luan" +local Time = require "luan:Time.luan" +local Lucene = require "luan:lucene/Lucene.luan" +local Http = require "luan:http/Http.luan" + + +local dir = uri("site:/private/local/lucene") + +local Db = Lucene.index( dir, { + log_dir = uri("site:/private/local/lucene_log") + name = "lucene" + version = 1 +} ) + +Db.indexed_fields.user_email = Lucene.type.lowercase + +Db.restore_from_log() + +if Http.is_serving then + Thread.schedule( Db.check, { delay=0, repeating_delay=Time.period{hours=1} } ) +end + +return Db
--- a/src/lib/Shared.luan Tue Jul 08 14:18:25 2025 -0600 +++ b/src/lib/Shared.luan Tue Jul 08 15:55:34 2025 -0600 @@ -1,6 +1,12 @@ local Luan = require "luan:Luan.luan" local error = Luan.error local Time = require "luan:Time.luan" +local Thread = require "luan:Thread.luan" +local thread_run = Thread.run or error() +local Mail = require "luan:mail/Mail.luan" +local User = require "site:/lib/User.luan" +local current_user = User.current or error() +local Config = require "site:/private/Config.luan" local Shared = {} @@ -10,6 +16,7 @@ function Shared.head() %> <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Lang</title> <style> @import "/site.css?s=<%=started%>"; </style> @@ -18,10 +25,35 @@ end function Shared.header() + local user = current_user() %> <div header> + <span> + <a href="/">Lang</a> + </span> + <span> +<% if user == nil then %> + <a href="/login.html">Login / Register</a> +<% else %> + <a href="/account.html"><%= user.email %></a> +<% end %> + </span> </div> <% end +local default_from = "Lang <lang@luan.software>" +local send_mail0 = Mail.sender(Config.mail_server).send +function Shared.send_mail(mail) + mail.From = mail.From or default_from + send_mail0(mail) +end + +function Shared.send_mail_async(mail) + mail.From = mail.From or default_from + thread_run( function() + send_mail0(mail) + end ) +end + return Shared
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/User.luan Tue Jul 08 15:55:34 2025 -0600 @@ -0,0 +1,129 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local range = Luan.range or error() +local to_string = Luan.to_string or error() +local get_local_only = Luan.get_local_only or error() +local set_local_only = Luan.set_local_only or error() +local String = require "luan:String.luan" +local sub_string = String.sub or error() +local Math = require "luan:Math.luan" +local random = Math.random or error() +local Table = require "luan:Table.luan" +local concat = Table.concat or error() +local Http = require "luan:http/Http.luan" +local Lucene = require "luan:lucene/Lucene.luan" +local lucene_quote = Lucene.quote or error() +local Db = require "site:/lib/Db.luan" +local run_in_transaction = Db.run_in_transaction or error() +local Utils = require "site:/lib/Utils.luan" +local base_url = Utils.base_url or error() + + +local User = {} + +local function from_doc(doc) + doc.type == "user" or error "wrong type" + return User.new { + id = doc.id + email = doc.user_email + password = doc.password + } +end + +local function to_doc(user) + return { + type = "user" + id = user.id + user_email = user.email or error() + password = user.password or error() + } +end + +function User.new(user) + + function user.save() + local doc = to_doc(user) + Db.save(doc) + user.id = doc.id + end + + function user.login() + local id = to_string(user.id) + Http.response.set_persistent_cookie("user",id) + Http.response.set_persistent_cookie("password",user.password) + Http.request.cookies.user = id + Http.request.cookies.password = user.password or error() + end + + function user.login_url() + return base_url().."/do_login.html?user="..user.id.."&password="..user.password + end + + return user +end + +local function get_by_id(id) + local doc = Db.get_document("id:"..id) + return doc and doc.type=="user" and from_doc(doc) or nil +end +User.get_by_id = get_by_id + +local password_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +do + local t = {} + for i in range(1,#password_chars) do + t[#t+1] = sub_string(password_chars,i,i) + end + password_chars = t +end + +local function new_password() + local n = #password_chars + local t = {} + for _ in range(1,10) do + t[#t+1] = password_chars[random(n)] + end + return concat(t) +end + +local function get_by_email(email) + local doc = Db.get_document("user_email:"..lucene_quote(email)) + return doc and from_doc(doc) +end +-- User.get_by_email = get_by_email + +function User.get_or_create_by_email(email) + return run_in_transaction( function() + local user = get_by_email(email) + if user == nil then + user = User.new{ + email = email + password = new_password() + notify_email = email + } + user.save() + end + return user + end ) +end + +local function current() + local user = get_local_only(User,"current") + if user == nil then + local id = Http.request.cookies.user + local password = Http.request.cookies.password + if id == nil or password == nil then + user = "nil" + else + user = get_by_id(id) + if user == nil or user.password ~= password then + user = "nil" + end + end + set_local_only(User,"current",user) + end + return user ~= "nil" and user or nil +end +User.current = current + +return User
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/Utils.luan Tue Jul 08 15:55:34 2025 -0600 @@ -0,0 +1,13 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local Http = require "luan:http/Http.luan" + + +local Utils = {} + +function Utils.base_url() + local request = Http.request + return request.scheme.."://"..request.headers["Host"] +end + +return Utils
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/ai/Ai.luan Tue Jul 08 15:55:34 2025 -0600 @@ -0,0 +1,13 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error + + +local Ai = {} + +local ai = "dummy" + +function Ai.require_ai(file) + return require("site:/private/lib/ai/"..ai.."/"..file) +end + +return Ai
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/login.html.luan Tue Jul 08 15:55:34 2025 -0600 @@ -0,0 +1,42 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local Io = require "luan:Io.luan" +local Http = require "luan:http/Http.luan" +local Shared = require "site:/lib/Shared.luan" +local head = Shared.head or error() +local header = Shared.header or error() + + +return function() + Io.stdout = Http.response.text_writer() +%> +<!doctype html> +<html lang="en"> + <head> +<% head() %> + <style> + input[name=email] { + width: 300px; + max-width: 100%; + } + </style> + </head> + <body> +<% header() %> + <div content> + <h1>Login / Register</h1> + <p>A link to login will be emailed to you.</p> + <form page onsubmit="ajaxForm('/login.js',this)" action="javascript:"> + <p> + <label prompt>Your email address</label> + <input type=email name=email required autofocus> + </p> + <p> + <input type=submit> + </p> + </form> + </div> + </body> +</html> +<% +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/login.js.luan Tue Jul 08 15:55:34 2025 -0600 @@ -0,0 +1,27 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local Io = require "luan:Io.luan" +local Http = require "luan:http/Http.luan" +local Shared = require "site:/lib/Shared.luan" +local send_mail_async = Shared.send_mail_async or error() +local User = require "site:/lib/User.luan" + + +return function() + local email = Http.request.parameters.email or error() + local user = User.get_or_create_by_email(email) + local url = user.login_url() + send_mail_async { + To = email + Subject = "Login" + body = `%> +Here is the link to login: + +<%= url %> +<% ` + } + Io.stdout = Http.response.text_writer() +%> + location = '/login_sent.html'; +<% +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/login_sent.html.luan Tue Jul 08 15:55:34 2025 -0600 @@ -0,0 +1,27 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local Io = require "luan:Io.luan" +local Http = require "luan:http/Http.luan" +local Shared = require "site:/lib/Shared.luan" +local head = Shared.head or error() +local header = Shared.header or error() + + +return function() + Io.stdout = Http.response.text_writer() +%> +<!doctype html> +<html lang="en"> + <head> +<% head() %> + </head> + <body> +<% header() %> + <div content> + <h1>Login / Register</h1> + <p>A link to login has been emailed to you.</p> + </div> + </body> +</html> +<% +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/private/Config_sample.luan Tue Jul 08 15:55:34 2025 -0600 @@ -0,0 +1,8 @@ +return { + mail_server = { + host = "mail.smtp2go.com" + port = 465 + username = "xxx" + password = "xxx" + } +}
--- a/src/site.css Tue Jul 08 14:18:25 2025 -0600 +++ b/src/site.css Tue Jul 08 15:55:34 2025 -0600 @@ -14,6 +14,14 @@ text-decoration: underline; } +div[header] { + font-size: 14px; + background-color: #ddd; + padding: 8px 3%; + display: flex; + justify-content: space-between; +} + [content] { margin-left: 3%; margin-right: 3%;
--- a/src/site.js Tue Jul 08 14:18:25 2025 -0600 +++ b/src/site.js Tue Jul 08 15:55:34 2025 -0600 @@ -29,6 +29,47 @@ request.send(postData); } +window.onerror = function(msg, url, line, col, error) { + if( !url ) + return; + let err = msg; + err += '\nurl = ' + url; + if( url != window.location ) + err += '\npage = ' + window.location; + err += '\nline = '+line; + if( col ) + err += '\ncolumn = ' + col; + if( error ) { + if( error.stack ) + err += '\nstack = ' + error.stack; + if( error.cause ) + err += '\ncause= ' + error.cause; + if( error.fileName ) + err += '\nfileName= ' + error.fileName; + } + if( window.err ) { + err += window.err; + window.err = null; + } + ajax( '/error_log.js', 'err='+encodeURIComponent(err) ); +}; + +function ajaxForm(url,form) { + let post = ''; + for( let i=0; i<form.length; i++ ) { + let input = form[i]; + let name = input.name; + if( name === '' ) + continue; + let type = input.type; + if( (type==='radio' || type==='checkbox') && !input.checked ) + continue; + post += name + '=' + encodeURIComponent(input.value) + '&'; + } + ajax(url,post,{form:form}); +} + + function showWaitingAiIcon() { document.querySelector('[waiting-ai-icon]').style.display = 'block'; }