commit d881a4e9482429b17c10d9417b4d4fe4bd21da9e Author: Luka Vandervelden Date: Thu Jan 2 10:40:25 2020 +0100 Initial commit. diff --git a/authws.ls b/authws.ls new file mode 100644 index 0000000..d0f2eb7 --- /dev/null +++ b/authws.ls @@ -0,0 +1,141 @@ +bulma = require "./bulma.ls" +h = require 'maquette' .h + +AuthWS = (socket-url) -> + self = {} + + request-types = { + "get-token": 0 + "add-user": 1 + "get-user": 2 + "get-user-by-credentials": 3 + "mod-user": 4 + "register": 5 + "get-extra": 6 + "set-extra": 7 + "update-password": 8 + "list-users": 9 + } + + response-types = { + "error": 0 + "token": 1 + "user": 2 + "user-added": 3 + "user-edited": 4 + "extra": 5 + "extra-updated": 6 + "users-list": 7 + } + + # TODO: naming convention + # users can record functions to run on events + self.user-on-socket-error = [] + self.user-on-socket-close = [] + + self.callbacks = {} + for key, value of response-types + self.callbacks[value] = [] + + self.add-event-listener = (type, callback) -> + type = response-types[type] + + self.callbacks[type] ++= [callback] + + self.open-socket = -> + self.socket := new WebSocket socket-url + + self.socket.onerror = (event) -> + for f in self.user-on-socket-error + f event + self.socket.close! + + self.socket.onclose = (event) -> + for f in self.user-on-socket-close + f event + + self.socket.onmessage = (event) -> + message = JSON.parse(event.data) + + for f in self.callbacks[message.mtype] + f JSON.parse(message.payload) + + self.reopen = -> + self.socket.close! + self.open-socket! + + self.open-socket! + + self.send = (type, opts) -> + self.socket.send JSON.stringify { mtype: type, payload: opts } + + self.get-token = (login, password) -> + self.send request-types[\get-token], JSON.stringify { + login: login + password: password + } + + self.get-user-by-credentials = (login, password) -> + self.send request-types[\get-user-by-credentials], JSON.stringify { + login: login + password: password + } + + self.login = (login, password) -> + self.get-token login, password + self.get-user-by-credentials login, password + + + self.get-user = (uid) -> + self.send request-types[\get-user], JSON.stringify { + uid: uid + } + + self.register = (login, password) -> + self.send request-types[\register], JSON.stringify { + login: login + password: password + } + + self.get-extra = (token, name) -> + self.send request-types[\get-extra], JSON.stringify { + token: token + name: name + } + + self.set-extra = (token, name, extra) -> + self.send request-types[\set-extra], JSON.stringify { + token: token + name: name + extra: extra + } + + self.update-password = (login, old-password, new-password) -> + self.send request-types[\update-password], JSON.stringify { + login: login + old_password: old-password + new_password: new-password + } + + self.list-users = (token) -> + self.send request-types[\list-users], JSON.stringify { + token: token + } + + # TODO: authd overhaul required + #self.add-user = (login, password) -> + # self.send request-types[\add-user], JSON.stringify { + # login: login + # password: password + # } + + # TODO: authd overhaul required + #self.mod-user = (uid) -> + # self.send request-types[\mod-user], JSON.stringify { + # uid: uid + # } + + self + +module.exports = AuthWS + diff --git a/bulma.ls b/bulma.ls new file mode 100644 index 0000000..ad315a6 --- /dev/null +++ b/bulma.ls @@ -0,0 +1,39 @@ + +h = require 'maquette' .h + +module.exports = { + box: (args, children) -> + h \div.box args, children + title: (level, args, label) -> + if not label + label = args + args = {} + + h "div.title.is-#{level}", args, [label] + label: (args, label) -> + if not label + label = args + args = {} + + h \label.label args, [label] + input: (args, children) -> + h \input.input args, children + + # FIXME: Use only args and add args.label and args.input? + # Or maybe args.name and args.type could be used directly? + field: (args, children) -> + h \div.field args, children + + modal: (args, content) -> + h \div.modal args, [ + h \div.modal-background args.background + h \div.modal-content [args.content] + ] + + form: (method, url, content) -> + h \form.form { + action: url + method: method + }, content +} + diff --git a/index.html b/index.html new file mode 100644 index 0000000..0417d01 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + AuthD + + + + + + + + diff --git a/index.ls b/index.ls new file mode 100644 index 0000000..602e3e3 --- /dev/null +++ b/index.ls @@ -0,0 +1,92 @@ +maquette = require "maquette" + +{create-projector, h} = maquette + +projector = create-projector! + +bulma = require "./bulma.ls" + +AuthWS = require "./authws.ls" + +LoginForm = require "./login-form.ls" +UserConfigurationPanel = require "./user-configuration-panel.ls" +UserAdminPanel = require "./user-admin-panel.ls" + +model = { + token: void +} + +authws-url = "ws://localhost:9999/auth.JSON" + +document.add-event-listener \DOMContentLoaded -> + user-config-panel = void + user-admin-panel = void + + login-form = LoginForm { + enable-registration: true + authws-url: authws-url + + on-login: (user, token) -> + model.user := user + model.token := token + + if false # user.groups.find (== "authd") + user-admin-panel := UserAdminPanel { + authws-url: authws-url + user: model.user + token: model.token + + on-model-update: -> + projector.schedule-render! + on-logout: -> + model.token := void + model.user := void + } + else + user-config-panel := UserConfigurationPanel { + authws-url: authws-url + user: model.user + token: model.token + + on-model-update: -> + projector.schedule-render! + on-logout: -> + model.token := void + model.user := void + } + + projector.schedule-render! + on-error: (error) -> + projector.schedule-render! + } + + projector.append document.body, -> + h \div.body [ + if model.token == void + h \div.section.hero.is-fullheight [ + h \div.hero-body [ + h \div.container [ + h \div.columns [ + h \div.column [] + h \div.column.is-3 [ + login-form.render! + ] + h \div.column [] + ] + ] + ] + ] + else if user-config-panel + h \div.section [ + h \div.container [ + user-config-panel.render! + ] + ] + else if user-admin-panel + h \div.section [ + h \div.container [ + user-admin-panel.render! + ] + ] + ] + diff --git a/login-form.ls b/login-form.ls new file mode 100644 index 0000000..431e7ee --- /dev/null +++ b/login-form.ls @@ -0,0 +1,222 @@ +maquette = require "maquette" + +{h} = maquette + +bulma = require "./bulma.ls" + +AuthWS = require "./authws.ls" + +LoginForm = (args) -> + args or= {} + + self = { + on-login: args.on-login || -> + on-error: args.on-error || -> + schedule-render: args.schedule-render || -> + current-view: "login" + + enable-registration: args.enable-registration || false + registrating: false + + input: { + login: "" + password: "" + repeat-password: "" + } + locked-input: true + + error: void + + authws-url: args.authws-url || + ((if location.protocol == 'https' then 'wss' else 'ws') + + '://' + location.hostname + ":9999/auth.JSON") + } + + + auth-ws = AuthWS self.authws-url + + auth-ws.socket.onopen = -> + console.log "socket is open" + self.locked-input := false + self.schedule-render! + + auth-ws.user-on-socket-error.push (...) -> + self.error = "socket error" + self.on-error ... + + auth-ws.add-event-listener \token, (message) -> + self.error := void + + self.token = message.token + self.locked-input := false + + if self.user + self.on-login self.user, self.token + + auth-ws.add-event-listener \user, (message) -> + self.error := void + + self.user = message.user + + if self.token + self.on-login self.user, self.token + + auth-ws.add-event-listener \user-added, (message) -> + {login, password} = {self.input.login, self.input.password} + + console.log "user added, duh" + + self.user := message.user + + auth-ws.get-token login, password + + auth-ws.add-event-listener \error, (message) -> + # We’ll get another error that’s clearer. Dropping that one. + if message.reason == "user not found" + return + + self.error := message.reason + self.locked-input := false + + self.on-error message.reason + + self.render = -> + if self.error == "socket error" + return h \div.notification.is-danger [ + h \div.title.is-4 [ "WebSocket error!" ] + h \p [ "Cannot connect to authd." ] + ] + + h \form.form.login-form { + key: self + onsubmit: (e) -> + {login, password} = {self.input.login, self.input.password} + + self.locked-input := true + + if self.registrating + auth-ws.register login, password + else + auth-ws.get-token login, password + auth-ws.get-user-by-credentials login, password + + e.prevent-default! + }, [ + h \div.field {key: \login} [ + bulma.label "Login" + bulma.input { + type: "text" + id: "login" + name: "login" + classes: { + "is-danger": self.error == "invalid credentials" + } + disabled: self.locked-input + oninput: (e) -> + self.input.login = e.target.value + } + ] + + h \div.field {key: \password} [ + bulma.label "Password" + bulma.input { + type: "password" + id: "password" + name: "password" + classes: { + "is-danger": self.error == "invalid credentials" + } + oninput: (e) -> + self.input.password = e.target.value + disabled: self.locked-input + } + ] + + if self.registrating + h \div.field {key: \password-repeat} [ + bulma.label "Password (reapeat)" + bulma.input { + type: \password + id: \password-repeat + name: \password-repeat + classes: { + "is-danger": self.input.password != self.input.repeat-password + } + disabled: self.locked-input + oninput: (e) -> + self.input.repeat-password = e.target.value + } + ] + + if self.error + h \div.field {key: \error-notification} [ + h \div.notification.is-danger [ + self.error + ] + ] + + if self.registrating + h \div.field.is-grouped {key: \login-button} [ + if self.input.login == "" + h \button.button.is-static.is-fullwidth { + type: \submit + } [ + "(empty login)" + ] + else if self.input.password != self.input.repeat-password + h \button.button.is-static.is-fullwidth { + type: \submit + } [ + "(passwords don’t match)" + ] + else if self.input.password == "" + h \button.button.is-static.is-fullwidth { + type: \submit + } [ + "(empty password)" + ] + else + h \button.button.is-success.is-fullwidth { + type: \submit + } [ + "Register!" + ] + ] + else + h \div.field.is-grouped {key: \login-button} [ + h \button.button.is-fullwidth.is-success { + type: \submit + } [ + "Log in!" + ] + ] + + h \div.field.level {key: \extra-buttons} [ + #h \div.level-left [ + # h \a.link [ "(lala, remember me?)" ] + #] + + if self.enable-registration + h \div.level-right [ + if self.registrating + h \a.link { + onclick: (e) -> + self.registrating := false + } [ + "Log in" + ] + else + h \a.link { + onclick: (e) -> + self.registrating := true + } [ + "Create account!" + ] + ] + ] + ] + + self + +module.exports = LoginForm + diff --git a/style.sass b/style.sass new file mode 100644 index 0000000..78c4ff6 --- /dev/null +++ b/style.sass @@ -0,0 +1,9 @@ +@charset "utf-8" + +//@import "../node_modules/bulmaswatch/superhero/_variables.scss" + +// Import Bulma core +@import "../node_modules/bulma/bulma" + +//@import "../node_modules/bulmaswatch/superhero/_overrides.scss" + diff --git a/user-admin-panel.ls b/user-admin-panel.ls new file mode 100644 index 0000000..69fa625 --- /dev/null +++ b/user-admin-panel.ls @@ -0,0 +1,63 @@ +{h} = require "maquette" + +AuthWS = require "./authws.ls" + +UserAdminPanel = (args) -> + self = { + token: args.token + authws-url: args.authws-url + on-logout: args.on-logout || -> + on-model-update: args.on-model-update || -> + users: [] + } + + authws = AuthWS self.authws-url + + authws.socket.onopen = -> + authws.list-users self.token + + authws.add-event-listener \users-list (message) -> + self.users = message.users + + self.on-model-update! + + self.render = -> + h \div.section [ + h \div.container [ + h \table.table.is-fullwidth [ + h \thead [ + h \tr [ + h \th [ "Login" ] + h \th [ "UID" ] + h \th [ "GID" ] + ] + ] + h \tbody [ + for user in self.users + h \tr {key: user.uid} [ + h \td [ + user.login + ] + h \td [ + user.uid.toString! + ] + h \td [ + user.gid.toString! + ] + ] + ] + ] + ] + h \div.button { + onclick: -> + self.on-logout! + self.on-model-update! + } [ + "Log out" + ] + ] + + self + +module.exports = UserAdminPanel + diff --git a/user-configuration-panel.ls b/user-configuration-panel.ls new file mode 100644 index 0000000..82472bd --- /dev/null +++ b/user-configuration-panel.ls @@ -0,0 +1,272 @@ + +{h} = require "maquette" + +AuthWS = require "./authws.ls" + +get-full-name = (self) -> + full-name = self.profile?.full-name || "" + if full-name == "" + self.user.login + else + full-name + +default-side-bar-renderer = (self) -> + h \div { key: \side-bar } [ + h \figure.image.is-128x128.is-clipped [ + if avatar = self.profile?.avatar + h \img { + src: avatar + alt: "Avatar of #{get-full-name self}" + } + ] + ] + +default-heading-renderer = (self) -> + full-name = get-full-name self + + h \div.section {key: \heading} [ + h \div.media [ + h \div.media-content [ + h \div.title.is-2 [ full-name ] + + if full-name != self.user.login + h \div.title.is-3.subtitle [ + self.user.login + ] + ] + + if self.on-logout + h \div.media-right [ + h \a { + onclick: -> + self.on-logout! + } [ "Logout" ] + ] + ] + ] + + +Fields = { + render-text-input: (token, auth-ws, key, inputs, model, on-request) -> + upload = -> + console.log "clickity click", key, inputs[key], inputs + return unless inputs[key] + + payload = {} + for _key, value of model + payload[_key] = value + payload[key] = inputs[key] + + inputs[key] := void + + on-request! + auth-ws.set-extra token, "profile", payload + + h \div.field.has-addons {key: key} [ + h \div.control.is-expanded [ + h \input.input { + value: inputs[key] || model[key] + oninput: (e) -> + console.log "input for",key + inputs[key] := e.target.value + } + ] + h \div.control [ + h \div.button { + onclick: upload + } [ "Update" ] + ] + ] +} + +UserConfigurationPanel = (args) -> + self = { + user: args.user || {} + profile: args.profile + token: args.token + authws-url: args.authws-url || + ((if location.protocol == 'https' then 'wss' else 'ws') + + '://' + location.hostname + ":9999/auth.JSON") + + side-bar-renderer: args.side-bar-renderer || default-side-bar-renderer + heading-renderer: args.heading-renderer || default-heading-renderer + + on-model-update: args.on-model-update || -> + on-logout: args.on-logout || void + + model: args.model || [ + ["fullName", "Full Name", "string"] + ["avatar", "Profile Picture", "image-url"] + ["email", "Mail Address", "string"] + ] + + input: {} + } + + auth-ws = AuthWS self.authws-url + + auth-ws.add-event-listener \extra, (message) -> + if message.name == "profile" + console.log "got profile", message.extra + self.profile = message.extra || {} + + self.on-model-update! + + auth-ws.add-event-listener \extra-updated, (message) -> + if message.name == "profile" + console.log "got profile", message.extra + self.profile = message.extra || {} + + self.on-model-update! + + auth-ws.add-event-listener \error, (message) -> + self.error := message.reason + + self.on-model-update! + + # Profile updates would be a \extra-updated, so this is specific to the + # password. + auth-ws.add-event-listener \user-edited, (message) -> + self.input["password.old"] := void + self.input["password.new"] := void + self.input["password.new2"] := void + self.success := "password" + self.on-model-update! + + unless self.profile + auth-ws.socket.onopen = -> + auth-ws.get-extra self.token, "profile" + + self.render = -> + h \div.columns { + key: self + } [ + h \div.column.is-narrow [ + self.side-bar-renderer self + ] + h \div.column [ + self.heading-renderer self + + if self.profile + h \div.box {key: \profile} [ + h \div.form [ + h \div.title.is-4 [ "Profile" ] + + for element in self.model + [key, label, type] = element + + switch type + when "string", "image-url" + h \div.field { key: key } [ + h \div.label [ label ] + Fields.render-text-input self.token, auth-ws, key, self.input, self.profile, (-> self.error := void) + ] + ] + ] + else + # FIXME: urk, ugly loader. + h \div.button.is-loading + + h \div.box { key: \password } [ + h \div.title.is-4 [ "Password" ] + h \div.label [ "Old password" ] + h \div.control [ + h \input.input { + type: \password + value: self.input["password.old"] + oninput: (e) -> + self.input["password.old"] = e.target.value + } + + if self.error == "invalid credentials" + h \div.help.is-danger [ + "The old password was invalid!" + ] + ] + h \div.label [ "New password" ] + h \div.control [ + h \input.input { + type: \password + value: self.input["password.new"] + oninput: (e) -> + self.input["password.new"] = e.target.value + } + ] + h \div.label [ "New password (repeat)" ] + h \div.field.has-addons [ + h \div.control.is-expanded [ + h \input.input { + type: \password + value: self.input["password.new2"] + oninput: (e) -> + self.input["password.new2"] = e.target.value + } + ] + h \div.control [ + h \div.button { + classes: { + "is-danger": self.input["password.new"] && self.input["password.new"] != self.input["password.new2"] + "is-static": (!self.input["password.new"]) && self.input["password.new"] != self.input["password.new2"] + } + onclick: -> + if self.input["password.new"] != self.input["password.new2"] + return + + self.error := void + auth-ws.update-password self.user.login, self.input["password.old"], self.input["password.new"] + + } [ "Update" ] + ] + ] + + if self.success == "password" + h \div.help.is-success [ + "Password successfully updated!" + ] + ] + + if self.show-developer + h \div.box {key: \passwd} [ + h \div.title.is-4 [ "Permissions" ] + + h \div.form [ + h \div.field {key: \uid} [ + h \div.label [ "User ID" ] + h \div.control [ self.user.uid.to-string! ] + ] + + h \div.field {key: \gid} [ + h \div.label [ "Group ID" ] + h \div.control [ self.user.gid.to-string! ] + ] + + h \div.field {key: \permissions} [ + h \div.label [ "Permissions" ] + h \div.control.is-grouped [ + h \div.tags [ + for service, permissions of self.user.permissions + permissions.map (perm) -> + h \div.tag [ + service, permission + ] + ] + ] + ] + ] + ] + else + h \a.is-pulled-right.is-small.has-text-grey { + key: \passwd + onclick: -> + self.show-developer := true + self.on-model-update! + } [ + "Show developer data!" + ] + ] + ] + + self + +module.exports = UserConfigurationPanel +