diff --git a/client/authws.ls b/client/authws.ls index 4824b32..dee9a1f 100644 --- a/client/authws.ls +++ b/client/authws.ls @@ -11,6 +11,8 @@ AuthWS = (socket-url) -> "get-user-by-credentials": 3 "mod-user": 4 "register": 5 + "get-extra": 6 + "set-extra": 7 } response-types = { @@ -19,6 +21,8 @@ AuthWS = (socket-url) -> "user": 2 "user-added": 3 "user-edited": 4 + "extra": 5 + "extra-updated": 6 } # TODO: naming convention @@ -90,6 +94,19 @@ AuthWS = (socket-url) -> 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 + } + # TODO: authd overhaul required #self.add-user = (login, password) -> # self.send request-types[\add-user], JSON.stringify { diff --git a/client/index.ls b/client/index.ls index c94769d..0ff8c67 100644 --- a/client/index.ls +++ b/client/index.ls @@ -15,18 +15,26 @@ model = { token: void } +authws-url = "ws://localhost:9999/auth.JSON" + document.add-event-listener \DOMContentLoaded -> user-config-panel = void login-form = LoginForm { enable-registration: true - authws-url: "ws://localhost:9999/auth.JSON" + authws-url: authws-url on-login: (user, token) -> model.user := user model.token := token - user-config-panel := UserConfigurationPanel model.user, model.token + user-config-panel := UserConfigurationPanel { + authhw-url: authws-url + user: model.user + token: model.token + + on-model-update: -> projector.schedule-render! + } projector.schedule-render! on-error: (error) -> @@ -52,9 +60,7 @@ document.add-event-listener \DOMContentLoaded -> else if user-config-panel h \div.section [ h \div.container [ - h \div.box [ - user-config-panel.render! - ] + user-config-panel.render! ] ] ] diff --git a/client/user-configuration-panel.ls b/client/user-configuration-panel.ls index f123e52..aa37a77 100644 --- a/client/user-configuration-panel.ls +++ b/client/user-configuration-panel.ls @@ -1,39 +1,156 @@ {h} = require "maquette" -UserConfigurationPanel = (user, token) -> - self = {} +AuthWS = require "./authws.ls" - console.log user +get-full-name = (self) -> + full-name = self.profile && 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 self.profile && self.profile.avatar + h \img { + src: self.profile.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.title.is-2 [ full-name ] + + if full-name != self.user.login + h \div.title.is-3.subtitle [ + self.user.login + ] + ] + + +Fields = { + render-text-input: (token, auth-ws, key, inputs, model) -> + 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 + + 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 || -> + + 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! + + unless self.profile + auth-ws.socket.onopen = -> + auth-ws.get-extra self.token, "profile" self.render = -> - full-name = user.full_name - if full-name == "" - full-name = user.login - h \div.columns { key: self } [ - h \div.column.is-one-quarter [ - h \figure.image.is-128 [ - h \img { - # FIXME - url: "https://bulma.io/images/placeholders/128x128.png" - alt: "Avatar of #{full-name}" - } - ] + h \div.column.is-narrow [ + self.side-bar-renderer self ] h \div.column [ - h \div.title.is-2 [ full-name ] + self.heading-renderer self - if full-name != user.login - h \div.title.is-3.subtitle [ - user.login + if self.profile + h \div.box {key: \profile} [ + h \div.form [ + h \div.title.is-4 [ "Profile" ] + h \div.label {key: \full-name-label} [ "Full Name" ] + Fields.render-text-input self.token, auth-ws, "fullName", self.input, self.profile + + h \div.label {key: \profile-picture-label} [ "Profile Picture" ] + Fields.render-text-input self.token, auth-ws, "avatar", self.input, self.profile + ] ] + else + # FIXME: urk, ugly loader. + h \div.button.is-loading - h \div.title.is-4 [ "Permissions" ] - h \div.tags user.groups.map (group) -> - h \div.tag [ group ] + 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: \groups} [ + h \div.label [ "Groups" ] + h \div.control.is-grouped [ + h \div.tags self.user.groups.map (group) -> + h \div.tag [ group ] + ] + ] + ] + ] ] ] diff --git a/shard.yml b/shard.yml index 175e606..513d755 100644 --- a/shard.yml +++ b/shard.yml @@ -24,5 +24,8 @@ dependencies: jwt: github: crystal-community/jwt branch: master + fs: + git: https://git.karchnu.fr/WeirdOS/fs.cr + branch: master license: EUPL diff --git a/src/authd.cr b/src/authd.cr index 825691b..f86dcf5 100644 --- a/src/authd.cr +++ b/src/authd.cr @@ -66,6 +66,22 @@ class AuthD::Response initialize :uid end + class Extra < Response + property user : Int32 + property name : String + property extra : JSON::Any? + + initialize :user, :name, :extra + end + + class ExtraUpdated < Response + property user : Int32 + property name : String + property extra : JSON::Any? + + initialize :user, :name, :extra + end + # This creates a Request::Type enumeration. One entry for each request type. {% begin %} enum Type @@ -191,6 +207,17 @@ class AuthD::Request initialize :login, :password end + class Request::GetExtra < Request + property token : String + property name : String + end + + class Request::SetExtra < Request + property token : String + property name : String + property extra : JSON::Any + end + # This creates a Request::Type enumeration. One entry for each request type. {% begin %} enum Type diff --git a/src/main.cr b/src/main.cr index 6b885f0..fb9fda2 100644 --- a/src/main.cr +++ b/src/main.cr @@ -5,6 +5,7 @@ require "openssl" require "jwt" require "passwd" require "ipc" +require "fs" require "./authd.cr" @@ -13,7 +14,7 @@ extend AuthD class AuthD::Service property registrations_allowed = false - def initialize(@passwd : Passwd, @jwt_key : String) + def initialize(@passwd : Passwd, @jwt_key : String, @extras_root : String) end def handle_request(request : AuthD::Request?, connection : IPC::Connection) @@ -80,11 +81,35 @@ class AuthD::Service user = @passwd.add_user request.login, request.password Response::UserAdded.new user + when Request::GetExtra + user = get_user_from_token request.token + + return Response::Error.new "invalid token" unless user + + storage = FS::Hash(String, JSON::Any).new "#{@extras_root}/#{user.uid}" + + Response::Extra.new user.uid, request.name, storage[request.name]? + when Request::SetExtra + user = get_user_from_token request.token + + return Response::Error.new "invalid token" unless user + + storage = FS::Hash(String, JSON::Any).new "#{@extras_root}/#{user.uid}" + + storage[request.name] = request.extra + + Response::ExtraUpdated.new user.uid, request.name, request.extra else Response::Error.new "unhandled request type" end end + def get_user_from_token(token) + user, meta = JWT.decode token, @jwt_key, JWT::Algorithm::HS256 + + Passwd::User.from_json user.to_json + end + def run ## # Provides a JWT-based authentication scheme for service-specific users. @@ -115,6 +140,7 @@ authd_passwd_file = "passwd" authd_group_file = "group" authd_jwt_key = "nico-nico-nii" authd_registrations = false +authd_extra_storage = "storage" OptionParser.parse do |parser| parser.on "-u file", "--passwd-file file", "passwd file." do |name| @@ -129,6 +155,10 @@ OptionParser.parse do |parser| authd_jwt_key = File.read(file_name).chomp end + parser.on "-S dir", "--extra-storage dir", "Storage for extra user-data." do |directory| + authd_extra_storage = directory + end + parser.on "-R", "--allow-registrations" do authd_registrations = true end @@ -142,7 +172,7 @@ end passwd = Passwd.new authd_passwd_file, authd_group_file -AuthD::Service.new(passwd, authd_jwt_key).tap do |authd| +AuthD::Service.new(passwd, authd_jwt_key, authd_extra_storage).tap do |authd| authd.registrations_allowed = authd_registrations end.run