diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
deleted file mode 100644
index 2a133a3..0000000
--- a/CONTRIBUTORS.md
+++ /dev/null
@@ -1,59 +0,0 @@
-SSOwat contributors
-===================
-
-YunoHost is built and maintained by the YunoHost project community.
-Everyone is encouraged to submit issues and changes, and to contribute in other ways -- see https://yunohost.org/contribute to find out how.
-
---
-
-SSOwat was initially built by Kload, for YunoHost v2.
-
-Design was created by Théodore 'Tozz' Faure and Thomas 'Courgette' Lebeau and implemented by Courgette himself.
-
-Most of code was written by Kload and opi, with help of numerous contributors.
-
-Translation is made by a bunch of lovely people over the world.
-
-We would like to thank anyone who ever helped the YunoHost project, and especially the SSOwat project <3
-
-
-SSOwat Contributors
--------------------
-
-- Kload
-- opi
-- Jérôme Lebleu
-- Maniack Crudelis
-- Julien 'ju' Malik
-- M5oul
-- Alexander Chalikiopoulos
-- Adrien 'Beudbeud' Beudin
-- Hnk Reno
-- Laurent 'Bram' Peuch
-- Loïc 'dzamlo' Damien
-- sidddy
-
-
-SSOwat Translators
-------------------
-
-### French
-
-- Jean-Baptiste Holcroft
-
-### German
-
-- Felix Bartels
-
-### Hindi
-
-- Anmol
-
-### Portuguese
-
-- Deleted User
-- Trollken
-
-### Spanish
-
-- Juanu
diff --git a/README.md b/README.md
index 21c3647..9db97de 100644
--- a/README.md
+++ b/README.md
@@ -7,29 +7,8 @@ A simple LDAP SSO for NGINX, written in Lua.
-Issues
-------
-
- [Please report issues to the YunoHost bugtracker](https://github.com/YunoHost/issues).
-Requirements
-------------
-
-- `nginx-extras` from Debian wheezy-backports
-- `lua-json`
-- `lua-ldap`
-- `lua-filesystem`
-- `lua-socket`
-- `lua-rex-pcre`
-
-**OR**
-
-- "OpenResty" flavored NGINX: https://openresty.org/
-- `lua-ldap`
-- `lua-filesystem`
-- `lua-socket`
-- `lua-rex-pcre`
-
Installation
------------
@@ -74,117 +53,15 @@ If you use YunoHost, you may want to edit the `/etc/ssowat/conf.json.persistent`
Only the `portal_domain` SSOwat configuration parameters is required, but it is recommended to know the others to fully understand what you can do with it.
----------------
+- `cookie_secret_file`: Where the secret used for signing and encrypting cookie is stored. It should only be readable by root.
+- `cookie_name`: The name of the cookie used for authentication. Its content is expected to be a JWT signed with the cookie secret and should contain a key `user` and `password` (which is needed for Basic HTTP Auth). Because JWT is only encoded and signed (not encrypted), the `password` is expected to be encrypted using the cookie secret.
+- `portal_domain`: Domain of the authentication portal. It has to be a domain, IP addresses will not work with SSOwat (**Required**).
+- `portal_path`: URI of the authentication portal (**default**: `/ssowat/`). This path **must** end with “`/`”.
+- `domains`: List of handled domains (**default**: similar to `portal_domain`).
+- `redirected_urls`: Array of URLs and/or URIs to redirect and their redirect URI/URL (**example**: `{ "/": "example.org/subpath" }`).
+- `redirected_regex`: Array of regular expressions to be matched against URLs **and** URIs and their redirect URI/URL (**example**: `{ "example.org/megusta$": "example.org/subpath" }`).
-### portal_domain
-
-Domain of the authentication portal. It has to be a domain, IP addresses will not work with SSOwat (**Required**).
-
----------------
-
-### portal_path
-
-URI of the authentication portal (**default**: `/ssowat/`). This path **must** end with “`/`”.
-
----------------
-
-### portal_port
-
-Web port of the authentication portal (**default**: `443` for `https`, `80` for `http`).
-
----------------
-
-### portal_scheme
-
-Whether authentication should use secure connection or not (**default**: `https`).
-
----------------
-
-### domains
-
-List of handled domains (**default**: similar to `portal_domain`).
-
----------------
-
-### ldap_host
-
-LDAP server hostname (**default**: `localhost`).
-
----------------
-
-### ldap_group
-
-LDAP group to search in (**default**: `ou=users,dc=yunohost,dc=org`).
-
----------------
-
-### ldap_identifier
-
-LDAP user identifier (**default**: `uid`).
-
----------------
-
-### ldap_attributes
-
-User's attributes to fetch from LDAP (**default**: `["uid", "givenname", "sn", "cn", "homedirectory", "mail", "maildrop"]`).
-
----------------
-
-### ldap_enforce_crypt
-
-Let SSOwat re-encrypt weakly-encrypted LDAP passwords into the safer sha-512 (crypt) (**default**: `true`).
-
----------------
-
-### allow_mail_authentication
-
-Whether users can authenticate with their mail address (**default**: `true`).
-
----------------
-
-### login_arg
-
-URI argument to use for cross-domain authentication (**default**: `sso_login`).
-
----------------
-
-### additional_headers
-
-Array of additionnal HTTP headers to set once user is authenticated (**default**: `{ "Remote-User": "uid" }`).
-
----------------
-
-### session_timeout
-
-The session expiracy time limit in seconds, since the last connection (**default**: `86400` / one day).
-
----------------
-
-### session_max_timeout
-
-The session expiracy time limit in seconds (**default**: `604800` / one week).
-
----------------
-
-### redirected_urls
-
-Array of URLs and/or URIs to redirect and their redirect URI/URL (**example**: `{ "/": "example.org/subpath" }`).
-
----------------
-
-### redirected_regex
-
-Array of regular expressions to be matched against URLs **and** URIs and their redirect URI/URL (**example**: `{ "example.org/megusta$": "example.org/subpath" }`).
-
----------------
-
-### default_language
-
-Language code used by default in views (**default**: `en`).
-
----------------
-
-### permissions
+### `permissions`
The list of permissions depicted as follows:
diff --git a/access.lua b/access.lua
index ea1021c..229403a 100644
--- a/access.lua
+++ b/access.lua
@@ -2,273 +2,155 @@
-- access.lua
--
-- This file is executed at every request on a protected domain or server.
--- You just have to read this file normally to understand how and when the
--- request is handled: redirected, forbidden, bypassed or served.
--
--- Get the `cache` persistent shared table
-local cache = ngx.shared.cache
-
--- Generate a unique token if it has not been generated yet
-srvkey = cache:get("srvkey")
-if not srvkey then
- srvkey = random_string()
- cache:add("srvkey", srvkey)
-end
-
--- Import helpers
-local hlp = require "helpers"
-
--- Initialize and get configuration
-hlp.refresh_config()
-local conf = hlp.get_config()
-
--- Load logging module
-local logger = require("log")
-
-- Just a note for the client to know that he passed through the SSO
ngx.header["X-SSO-WAT"] = "You've just been SSOed"
-local is_logged_in = hlp.refresh_logged_in()
+-- Misc imports
+local jwt = require("vendor.luajwtjitsi.luajwtjitsi")
+local cipher = require('openssl.cipher')
+local rex = require("rex_pcre2")
---
--- 1. LOGIN
---
--- example: https://mydomain.org/?sso_login=a6e5320f
---
--- If the `sso_login` URI argument is set, try a cross-domain authentication
--- with the token passed as argument
---
-if ngx.var.host ~= conf["portal_domain"] and ngx.var.request_method == "GET" then
- uri_args = ngx.req.get_uri_args()
- if uri_args[conf.login_arg] then
- cda_key = uri_args[conf.login_arg]
+-- ###########################################################################
+-- 0. Misc helpers because Lua has no sugar ...
+-- ###########################################################################
- -- Use the `cache` shared table where a username is associated with
- -- a CDA key
- user = cache:get("CDA|"..cda_key)
- if user then
- hlp.set_auth_cookie(user, ngx.var.host)
- logger.info("Cross-domain authentication: "..user.." connected on "..ngx.var.host)
- cache:delete("CDA|"..cda_key)
+-- Get configuration (we do this here, the conf is re-read every time unless
+-- the file's timestamp didnt change)
+local config = require("config")
+local conf = config.get_config()
+
+-- Cache expensive calculations
+local cache = ngx.shared.cache
+
+-- Hash a string using hmac_sha512, return a hexa string
+function cached_jwt_verify(data, secret)
+ res = cache:get(data)
+ if res == nil then
+ logger:debug("Result not found in cache, checking login")
+ -- Perform expensive calculation
+ decoded, err = jwt.verify(data, "HS256", cookie_secret)
+ if not decoded then
+ logger:error(err)
+ return nil, nil, err
end
-
- uri_args[conf.login_arg] = nil
- return hlp.redirect(ngx.var.uri..hlp.uri_args_string(uri_args))
- end
-end
-
-
---
--- 2. PORTAL
---
--- example: https://mydomain.org/ssowat*
---
--- If the URL matches the portal URL, serve a portal file or proceed to a
--- portal operation
---
-if (ngx.var.host == conf["portal_domain"] or is_logged_in)
- and hlp.string.starts(ngx.var.uri, string.sub(conf["portal_path"], 1, -2))
-then
-
- -- `GET` method will serve a portal file
- if ngx.var.request_method == "GET" then
-
- -- Force portal scheme
- if ngx.var.scheme ~= conf["portal_scheme"] then
- return hlp.redirect(conf.portal_url)
- end
-
- -- Add a trailing `/` if not present
- if ngx.var.uri.."/" == conf["portal_path"] then
- return hlp.redirect(conf.portal_url)
- end
-
- -- Get request arguments
- uri_args = ngx.req.get_uri_args()
-
- -- Logout is also called via a `GET` method
- -- TODO: change this ?
- if uri_args.action and uri_args.action == 'logout' then
- logger.debug("Logging out")
- return hlp.logout()
-
- -- If the `r` URI argument is set, it means that we want to
- -- be redirected (typically after a login phase)
- elseif is_logged_in and uri_args.r then
- -- Decode back url
- back_url = ngx.decode_base64(uri_args.r)
-
- -- If `back_url` contains line break, someone is probably trying to
- -- pass some additional headers
- if string.match(back_url, "(.*)\n") then
- hlp.flash("fail", hlp.t("redirection_error_invalid_url"))
- logger.error("Redirection url is invalid")
- return hlp.redirect(conf.portal_url)
- end
-
- -- Get managed domains
- local managed_domain = false
- for _, domain in ipairs(conf["domains"]) do
- local escaped_domain = domain:gsub("-", "%%-") -- escape dash for pattern matching
- if string.match(back_url, "^http[s]?://"..escaped_domain.."/") then
- logger.debug("Redirection to a managed domain found")
- managed_domain = true
- break
- end
- end
-
- -- If redirection does not match one of the managed domains
- -- redirect to portal home page
- if not managed_domain then
- hlp.flash("fail", hlp.t("redirection_error_unmanaged_domain"))
- logger.error("Redirection to an external domain aborted")
- return hlp.redirect(conf.portal_url)
- end
-
-
- -- In case the `back_url` is not on the same domain than the
- -- current one, create a redirection with a CDA key
- local ngx_host_escaped = ngx.var.host:gsub("-", "%%-") -- escape dash for pattern matching
- if not string.match(back_url, "^http[s]?://"..ngx_host_escaped.."/")
- and not string.match(back_url, ".*"..conf.login_arg.."=%d+$") then
- local cda_key = hlp.set_cda_key()
- if string.match(back_url, ".*?.*") then
- back_url = back_url.."&"
- else
- back_url = back_url.."?"
- end
- back_url = back_url.."sso_login="..cda_key
- end
-
- return hlp.redirect(back_url)
-
-
- -- In case we want to serve portal login or assets for portal, just
- -- serve it
- elseif is_logged_in
- or ngx.var.uri == conf["portal_path"]
- or (hlp.string.starts(ngx.var.uri, conf["portal_path"].."assets")
- and (not ngx.var.http_referer
- or hlp.string.starts(ngx.var.http_referer, conf.portal_url)))
- then
- -- If this is an asset, enable caching
- if hlp.string.starts(ngx.var.uri, conf["portal_path"].."assets")
- then
- return hlp.serve(ngx.var.uri, "static_asset")
- else
- return hlp.serve(ngx.var.uri)
- end
-
-
- -- If all the previous cases have failed, redirect to portal
- else
- hlp.flash("info", hlp.t("please_login"))
- logger.debug("User should log in to be able to access "..ngx.var.uri)
- -- Force the scheme to HTTPS. This is to avoid an issue with redirection loop
- -- when trying to access http://main.domain.tld/ (SSOwat finds that user aint
- -- logged in, therefore redirects to SSO, which redirects to the back_url, which
- -- redirect to SSO, ..)
- local back_url = "https://" .. ngx.var.host .. ngx.var.uri .. hlp.uri_args_string()
- return hlp.redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url))
- end
-
-
- -- `POST` method is basically use to achieve editing operations
- elseif ngx.var.request_method == "POST" then
-
- -- CSRF protection, only proceed if we are editing from the same
- -- domain
- if hlp.string.starts(ngx.var.http_referer, conf.portal_url) then
- if hlp.string.ends(ngx.var.uri, conf["portal_path"].."password.html")
- or hlp.string.ends(ngx.var.uri, conf["portal_path"].."edit.html")
- then
- logger.debug("User attempts to edit its information")
- return hlp.edit_user()
- else
- logger.debug("User attempts to log in")
- return hlp.login()
- end
- else
- -- Redirect to portal
- hlp.flash("fail", hlp.t("please_login_from_portal"))
- logger.debug("Invalid POST request not coming from the portal url...")
- return hlp.redirect(conf.portal_url)
- end
- end
-end
-
---
--- 2 ... continued : portal assets that are available on every domains
---
--- For example: `https://whatever.org/ynhpanel.js` will serve the
--- `/yunohost/sso/assets/js/ynhpanel.js` file.
---
-
-if is_logged_in then
- assets = {
- ["/ynh_portal.js"] = "js/ynh_portal.js",
- ["/ynh_userinfo.json"] = "ynh_userinfo.json",
- ["/ynh_overlay.css"] = "css/ynh_overlay.css"
- }
- theme_dir = "/usr/share/ssowat/portal/assets/themes/"..conf.theme
- local pfile = io.popen('find "'..theme_dir..'" -not -path "*/\\.*" -type f -exec realpath --relative-to "'..theme_dir..'" {} \\;')
- for filename in pfile:lines() do
- assets["/ynhtheme/"..filename] = "themes/"..conf.theme.."/"..filename
- end
- pfile:close()
-
- for shortcut, full in pairs(assets) do
- if ngx.var.uri == shortcut then
- logger.debug("Serving static asset "..full)
- return hlp.serve("/yunohost/sso/assets/"..full, "static_asset")
- end
- end
-end
-
-
---
--- 3. REDIRECTED URLS
---
--- If the URL matches one of the `redirected_urls` in the configuration file,
--- just redirect to the target URL/URI
---
-
-function detect_redirection(redirect_url)
- if hlp.string.starts(redirect_url, "http://")
- or hlp.string.starts(redirect_url, "https://") then
- return hlp.redirect(redirect_url)
- elseif hlp.string.starts(redirect_url, "/") then
- return hlp.redirect(ngx.var.scheme.."://"..ngx.var.host..redirect_url)
+ -- As explained in set_basic_auth_header(), user and hashed password do not contain ':'
+ -- And cache cannot contain tables, so we use "user:password" format
+ cached = decoded["user"]..":"..decoded["pwd"]
+ cache:set(data, cached, 120)
+ logger:debug("Result saved in cache")
+ return decoded["user"], decoded["pwd"], err
else
- return hlp.redirect(ngx.var.scheme.."://"..redirect_url)
+ logger:debug("Result found in cache")
+ user, pwd = res:match("([^:]+):(.*)")
+ return user, pwd, nil
+ end
+end
+
+-- The 'match' function uses PCRE regex as default
+-- If '%.' is found in the regex, we assume it's a LUA regex (legacy code)
+-- 'match' returns the matched text.
+function match(s, regex)
+ if not string.find(regex, '%%%.') then
+ return rex.match(s, regex)
+ else
+ return string.match(s,regex)
+ end
+end
+
+-- Test whether a string starts with another
+function string.starts(String, Start)
+ if not String then
+ return false
+ end
+ return string.sub(String, 1, string.len(Start)) == Start
+end
+
+-- Convert a table of arguments to an URI string
+function uri_args_string(args)
+ if not args then
+ args = ngx.req.get_uri_args()
+ end
+ String = "?"
+ for k,v in pairs(args) do
+ String = String..tostring(k).."="..tostring(v).."&"
+ end
+ return string.sub(String, 1, string.len(String) - 1)
+end
+
+-- ###########################################################################
+-- 1. AUTHENTICATION
+-- Check wether or not this is a logged-in user
+-- This is not run immediately but only if:
+-- - the app is not public
+-- - and/or auth_headers is enabled for this app
+-- ###########################################################################
+
+function check_authentication()
+
+ -- cf. src/authenticators/ldap_ynhuser.py in YunoHost to see how the cookie is actually created
+
+ local cookie = ngx.var["cookie_" .. conf["cookie_name"]]
+ if cookie == nil then
+ return false, nil, nil
+ end
+
+ user, pwd, err = cached_jwt_verify(cookie, cookie_secret)
+
+ -- FIXME : maybe also check that the cookie was delivered for the requested domain (or a parent?)
+
+ -- FIXME : we might want also a way to identify expired/invalidated cookies,
+ -- e.g. a user that got deleted after being logged in, or a user that logged out ...
+
+ if err ~= nil then
+ return false, nil, nil
+ else
+ return true, user, pwd
+ end
+end
+
+-- ###########################################################################
+-- 2. REDIRECTED URLS
+-- If the URL matches one of the `redirected_urls` in the configuration file,
+-- just redirect to the target URL/URI
+-- ###########################################################################
+
+function convert_to_absolute_url(redirect_url)
+ if string.starts(redirect_url, "http://")
+ or string.starts(redirect_url, "https://") then
+ return redirect_url
+ elseif string.starts(redirect_url, "/") then
+ return ngx.var.scheme.."://"..ngx.var.host..redirect_url
+ else
+ return ngx.var.scheme.."://"..redirect_url
end
end
if conf["redirected_urls"] then
for url, redirect_url in pairs(conf["redirected_urls"]) do
- if url == ngx.var.host..ngx.var.uri..hlp.uri_args_string()
- or url == ngx.var.scheme.."://"..ngx.var.host..ngx.var.uri..hlp.uri_args_string()
- or url == ngx.var.uri..hlp.uri_args_string() then
- logger.debug("Requested URI is in redirected_urls")
- detect_redirection(redirect_url)
+ if url == ngx.var.host..ngx.var.uri..uri_args_string()
+ or url == ngx.var.scheme.."://"..ngx.var.host..ngx.var.uri..uri_args_string()
+ or url == ngx.var.uri..uri_args_string() then
+ logger:debug("Found in redirected_urls, redirecting to "..url)
+ ngx.redirect(convert_to_absolute_url(redirect_url))
end
end
end
if conf["redirected_regex"] then
for regex, redirect_url in pairs(conf["redirected_regex"]) do
- if hlp.match(ngx.var.host..ngx.var.uri..hlp.uri_args_string(), regex)
- or hlp.match(ngx.var.scheme.."://"..ngx.var.host..ngx.var.uri..hlp.uri_args_string(), regex)
- or hlp.match(ngx.var.uri..hlp.uri_args_string(), regex) then
- logger.debug("Requested URI is in redirected_regex")
- detect_redirection(redirect_url)
+ if match(ngx.var.host..ngx.var.uri..uri_args_string(), regex)
+ or match(ngx.var.scheme.."://"..ngx.var.host..ngx.var.uri..uri_args_string(), regex)
+ or match(ngx.var.uri..uri_args_string(), regex) then
+ logger:debug("Found in redirected_regex, redirecting to "..url)
+ ngx.redirect(convert_to_absolute_url(redirect_url))
end
end
end
---
--- 4. IDENTIFY THE RELEVANT PERMISSION
+-- ###########################################################################
+-- 3. IDENTIFY PERMISSION MATCHING THE REQUESTED URL
--
-- In particular, the conf is filled with permissions such as:
--
@@ -287,7 +169,7 @@ end
--
-- And we find the best matching permission by trying to match the request uri
-- against all the uris rules/regexes from the conf and keep the longest matching one.
---
+-- ###########################################################################
permission = nil
longest_url_match = ""
@@ -305,7 +187,7 @@ for permission_name, permission_infos in pairs(conf["permissions"]) do
url = "^"..url
end
- local m = hlp.match(ngx_full_url, url)
+ local m = match(ngx_full_url, url)
if m ~= nil and string.len(m) > string.len(longest_url_match) then
longest_url_match = m
permission = permission_infos
@@ -315,65 +197,168 @@ for permission_name, permission_infos in pairs(conf["permissions"]) do
end
end
+-- ###########################################################################
+-- 4. CHECK USER HAS ACCESS
+-- Either because the permission is set as "public: true",
+-- Or because the logged-in user is listed in the "users" list of the perm
+-- ###########################################################################
----
---- 5. CHECK CLIENT-PROVIDED AUTH HEADER (should almost never happen?)
----
+function element_is_in_table(element, table)
+ if table then
+ for _, el in pairs(table) do
+ if el == element then
+ return true
+ end
+ end
+ end
-if permission ~= nil then
+ return false
+end
+
+-- Check whether the app is public access
+function check_public_access(permission)
+ if permission == nil then
+ logger:debug("No permission matching request for "..ngx.var.uri.." ... Assuming access is denied")
+ return false
+ end
+
+ if permission["public"] then
+ logger:debug("Someone tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
+ return true
+ end
+end
+
+-- Check whether a user is allowed to access a URL using the `permissions` directive
+-- of the configuration file
+function check_has_access(permission)
+
+ -- Public access
+ if authUser == nil or permission["public"] then
+ user = authUser or "A visitor"
+ logger:debug(user.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
+ return permission["public"]
+ end
+
+ logger:debug("User "..authUser.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
+
+ -- The user has permission to access the content if he is in the list of allowed users
+ if element_is_in_table(authUser, permission["users"]) then
+ logger:debug("User "..authUser.." can access "..ngx.var.host..ngx.var.uri..uri_args_string())
+ return true
+ else
+ logger:debug("User "..authUser.." cannot access "..ngx.var.uri)
+ return false
+ end
+end
+
+if check_public_access(permission) then
+ has_access = true
+else
+ is_logged_in, authUser, authPasswordEnc = check_authentication()
+ has_access = check_has_access(permission)
+end
+
+-- ###########################################################################
+-- 5. CLEAR USER-PROVIDED AUTH HEADER
+--
+-- Which could be spoofing attempts
+-- Unfortunately we can't yolo-clear them on every route because some
+-- apps use legit basic auth mechanism ...
+--
+-- "Remote user" refers to the fact that Basic Auth headers is coupled to
+-- the $remote_user var in nginx, typically used by PHP apps
+-- ###########################################################################
+
+if permission ~= nil and ngx.req.get_headers()["Authorization"] ~= nil then
perm_user_remote_user_var_in_nginx_conf = permission["use_remote_user_var_in_nginx_conf"]
if perm_user_remote_user_var_in_nginx_conf == nil or perm_user_remote_user_var_in_nginx_conf == true then
- is_logged_in_with_basic_auth = hlp.validate_or_clear_basic_auth_header_provided_by_client()
-
- -- NB: is_logged_in_with_basic_auth can be false, true or nil
- if is_logged_in_with_basic_auth == false then
- return ngx.exit(ngx.HTTP_UNAUTHORIZED)
- elseif is_logged_in_with_basic_auth == true then
- is_logged_in = true
+ -- Ignore if not a Basic auth header
+ -- otherwise, we interpret this as a Auth header spoofing attempt and clear it
+ local auth_header_from_client = ngx.req.get_headers()["Authorization"]
+ _, _, b64_cred = string.find(auth_header_from_client, "^Basic%s+(.+)$")
+ if b64_cred ~= nil then
+ ngx.req.clear_header("Authorization")
end
end
end
+-- ###########################################################################
+-- 6. EFFECTIVELY PASS OR DENY ACCESS
--
+-- If the user has access (either because app is public OR logged in + authorized)
+-- -> pass + possibly inject the Basic Auth header on the fly such that the app can know which user is logged in
--
--- 6. APPLY PERMISSION
---
---
+-- Otherwise, the user can't access
+-- -> either because not logged in at all, in that case, redirect to the portal WITH a callback url to redirect to after logging in
+-- -> or because user is logged in, but has no access .. in that case just redirect to the portal
+-- ###########################################################################
+
+function set_basic_auth_header()
+
+ -- cf. https://en.wikipedia.org/wiki/Basic_access_authentication
+
+ -- authPasswordEnc is actually a string formatted as |
+ -- For example: ctl8kk5GevYdaA5VZ2S88Q==|yTAzCx0Gd1+MCit4EQl9lA==
+ -- The password is encoded using AES-256-CBC with the IV being the right-side data
+ -- cf. src/authenticators/ldap_ynhuser.py in YunoHost to see how the cookie is actually created
+ local password_enc_b64, iv_b64 = authPasswordEnc:match("([^|]+)|([^|]+)")
+ local password_enc = ngx.decode_base64(password_enc_b64)
+ local iv = ngx.decode_base64(iv_b64)
+ local password = cipher.new('aes-256-cbc'):decrypt(cookie_secret, iv):final(password_enc)
+
+ -- Set `Authorization` header to enable HTTP authentification
+ ngx.req.set_header("Authorization", "Basic "..ngx.encode_base64(
+ authUser..":"..password
+ ))
+end
-- 1st case : client has access
-
-if hlp.has_access(permission) then
-
- if is_logged_in then
- -- If the user is logged in, refresh_cache
- hlp.refresh_user_cache()
-
- -- If Basic Authorization header are enable for this permission,
- -- add it to the response
- if permission["auth_header"] then
- hlp.set_headers()
- else
- hlp.clear_headers()
+if has_access then
+ -- If Basic Authorization header are enable for this permission,
+ -- check if the user is actually logged in...
+ if permission["auth_header"] then
+ if is_logged_in == nil then
+ -- Login check was not performed yet because the app is public
+ logger:debug("Checking authentication because the app requires auth_header")
+ is_logged_in, authUser, authPasswordEnc = check_authentication()
+ end
+ if is_logged_in then
+ -- add it to the response
+ set_basic_auth_header()
end
- else
- hlp.clear_headers()
end
- return hlp.pass()
+ -- Pass
+ logger:debug("Allowing to pass through "..ngx.var.uri)
+ return
-- 2nd case : no access ... redirect to portal / login form
else
- if is_logged_in then
- return hlp.redirect(conf.portal_url)
- else
- -- Only display this if HTTPS. For HTTP, we can't know if the user really is
- -- logged in or not, because the cookie is available only in HTTP...
- if ngx.var.scheme == "https" then
- hlp.flash("info", hlp.t("please_login"))
- end
+ portal_domain = conf["domain_portal_urls"][ngx.var.host]
+ if portal_domain == nil then
+ logger:debug("Domain " .. ngx.var.host .. " is not configured for SSOWat")
+ ngx.status = 400
+ ngx.header.content_type = "plain/text"
+ ngx.say("Unmanaged domain: " .. ngx.var.host)
+ return
+ end
+ portal_url = "https://" .. portal_domain
+ logger:debug("Redirecting to portal : " .. portal_url)
- local back_url = "https://" .. ngx.var.host .. ngx.var.uri .. hlp.uri_args_string()
- return hlp.redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url))
+ if is_logged_in then
+ return ngx.redirect(portal_url)
+ else
+ local back_url = "https://" .. ngx.var.host .. ngx.var.uri .. uri_args_string()
+
+ -- User ain't logged in, redirect to the portal where we expect the user to login,
+ -- then be redirected to the original URL by the portal, encoded as base64
+ --
+ -- NB. for security reason, the client/app handling the callback should check
+ -- that the back URL is legit, i.e it should be on the same domain (or a subdomain)
+ -- than the portal. Otherwise, a malicious actor could create a deceptive link
+ -- that would in fact redirect to a different domain, tricking the user that may
+ -- not realize this.
+ return ngx.redirect(portal_url.."?r="..ngx.encode_base64(back_url))
end
end
diff --git a/conf.json.example b/conf.json.example
index aceaea9..fa5ab05 100644
--- a/conf.json.example
+++ b/conf.json.example
@@ -1,13 +1,8 @@
{
- "additional_headers": {
- "Auth-User": "uid",
- "Email": "mail",
- "Name": "cn",
- "Remote-User": "uid"
- },
- "domains": [
- "example.tld",
- "example.org"
+ "domain_portal_urls": [
+ "example.tld": "example.tld/yunohost/sso",
+ "sub.example.tld": "example.tld/yunohost/sso",
+ "foobar.org": "foobar.org/yunohost/sso"
],
"permissions": {
"core_skipped": {
@@ -60,10 +55,8 @@
]
}
},
- "portal_domain": "example.tld",
- "portal_path": "/yunohost/sso/",
"redirected_regex": {
"example.tld/yunohost[\\/]?$": "https://example.tld/yunohost/sso/"
},
"redirected_urls": {}
-}
\ No newline at end of file
+}
diff --git a/config.lua b/config.lua
index f574af0..a7ac64f 100644
--- a/config.lua
+++ b/config.lua
@@ -6,11 +6,31 @@
module('config', package.seeall)
+local lfs = require("lfs")
+local json = require("json")
+
local config_attributes = nil
local config_persistent_attributes = nil
local conf = {}
+local conf_path = "/etc/ssowat/conf.json"
+
+
+function get_cookie_secret()
+
+ local conf_file = assert(io.open(conf_path, "r"), "Configuration file is missing")
+ local conf_ = json.decode(conf_file:read("*all"))
+ conf_file:close()
+
+ local cookie_secret_path = conf_["cookie_secret_file"] or "/etc/yunohost/.ssowat_cookie_secret"
+ local cookie_secret_file = assert(io.open(cookie_secret_path, "r"), "Cookie secret file is missing")
+ local cookie_secret = cookie_secret_file:read("*all")
+ cookie_secret_file:close()
+
+ return cookie_secret
+end
+
function compare_attributes(file_attributes1, file_attributes2)
if file_attributes1 == nil and file_attributes2 == nil then
return true
@@ -20,15 +40,6 @@ function compare_attributes(file_attributes1, file_attributes2)
return file_attributes1["modification"] == file_attributes2["modification"] and file_attributes1["size"] == file_attributes2["size"]
end
-function update_language()
- -- Set the prefered language from the `Accept-Language` header
- conf.lang = ngx.req.get_headers()["Accept-Language"]
-
- if conf.lang then
- conf.lang = string.sub(conf.lang, 1, 2)
- end
-end
-
function get_config()
-- Get config files attributes (timestamp modification and size)
@@ -36,11 +47,9 @@ function get_config()
local new_config_persistent_attributes = lfs.attributes(conf_path..".persistent", {"modification", "size"})
if compare_attributes(new_config_attributes, config_attributes) and compare_attributes(new_config_persistent_attributes, config_persistent_attributes) then
- update_language()
return conf
-- If the file is being written, its size may be 0 and reloading fails, return the last valid config
elseif new_config_attributes == nil or new_config_attributes["size"] == 0 then
- update_language()
return conf
end
@@ -78,55 +87,10 @@ function get_config()
end
end
-
- -- Default configuration values
- default_conf = {
- portal_scheme = "https",
- portal_path = "/ssowat/",
- local_portal_domain = "yunohost.local",
- domains = { conf["portal_domain"], "yunohost.local" },
- session_timeout = 60 * 60 * 24, -- one day
- session_max_timeout = 60 * 60 * 24 * 7, -- one week
- login_arg = "sso_login",
- ldap_host = "localhost",
- ldap_group = "ou=users,dc=yunohost,dc=org",
- ldap_identifier = "uid",
- ldap_enforce_crypt = true,
- skipped_urls = {},
- ldap_attributes = {"uid", "givenname", "sn", "cn", "homedirectory", "mail", "maildrop"},
- allow_mail_authentication = true,
- default_language = "en",
- theme = "default",
- logging = "fatal", -- Only log fatal messages by default (so apriori nothing)
- permissions = {}
- }
-
-
- -- Load default values unless they are set in the configuration file.
- for param, default_value in pairs(default_conf) do
- conf[param] = conf[param] or default_value
+ -- Always skip the portal urls to avoid redirection looping.
+ for domain, portal_url in pairs(conf["domain_portal_urls"]) do
+ table.insert(conf["permissions"]["core_skipped"]["uris"], portal_url)
end
-
-
- -- If you access the SSO by a local domain, change the portal domain to
- -- avoid unwanted redirections.
- if ngx.var.host == conf["local_portal_domain"] then
- conf["original_portal_domain"] = conf["portal_domain"]
- conf["portal_domain"] = conf["local_portal_domain"]
- end
-
-
- -- Build portal full URL out of the configuration values
- conf.portal_url = conf["portal_scheme"].."://"..
- conf["portal_domain"]..
- conf["portal_path"]
-
-
- -- Always skip the portal to avoid redirection looping.
- table.insert(conf["permissions"]["core_skipped"]["uris"], conf["portal_domain"]..conf["portal_path"])
-
- update_language()
-
return conf
end
diff --git a/debian/control b/debian/control
index ff60631..17a0a0f 100644
--- a/debian/control
+++ b/debian/control
@@ -7,7 +7,7 @@ Standards-Version: 3.9.1
Package: ssowat
Architecture: all
-Depends: nginx-extras (>=1.6.2), lua-ldap (>=1.3.1), lua-json, lua-rex-pcre2, lua-filesystem, lua-socket, whois
+Depends: nginx-extras (>=1.6.2), lua-json, lua-rex-pcre2, lua-basexx, lua-luaossl, lua-logging, whois
Homepage: https://yunohost.org
Description: user portal with single sign-on designed for Yunohost
A minimalist user portal with single sign-on, designed to be
diff --git a/helpers.lua b/helpers.lua
deleted file mode 100644
index eea03d8..0000000
--- a/helpers.lua
+++ /dev/null
@@ -1,1121 +0,0 @@
---
--- helpers.lua
---
--- This is a file called at every request by the `access.lua` file. It contains
--- a set of useful functions related to HTTP and LDAP.
---
-
-module('helpers', package.seeall)
-
-local cache = ngx.shared.cache
-local conf = config.get_config()
-local logger = require("log")
-
--- url parser, c.f. https://rosettacode.org/wiki/URL_parser#Lua
-local url_parser = require "socket.url"
-
--- Import Perl regular expressions library
-local rex = require "rex_pcre2"
-
-local is_logged_in = false
-
-function refresh_config()
- conf = config.get_config()
-end
-
-function get_config()
- return conf
-end
-
--- The 'match' function uses PCRE regex as default
--- If '%.' is found in the regex, we assume it's a LUA regex (legacy code)
--- 'match' returns the matched text.
-function match(s, regex)
- if not string.find(regex, '%%%.') then
- return rex.match(s, regex)
- else
- return string.match(s,regex)
- end
-end
-
--- Read a FS stored file
-function read_file(file)
- local f = io.open(file, "rb")
- if not f then return false end
- local content = f:read("*all")
- f:close()
- return content
-end
-
-
--- Lua has no sugar :D
-function is_in_table(t, v)
- for key, value in ipairs(t) do
- if value == v then return key end
- end
-end
-
-
--- Get the index of a value in a table
-function index_of(t,val)
- for k,v in ipairs(t) do
- if v == val then return k end
- end
-end
-
-
--- Test whether a string starts with another
-function string.starts(String, Start)
- if not String then
- return false
- end
- return string.sub(String, 1, string.len(Start)) == Start
-end
-
-
--- Test whether a string ends with another
-function string.ends(String, End)
- return End=='' or string.sub(String, -string.len(End)) == End
-end
-
-
--- Find a string by its translate key in the right language
-function t(key)
- if conf.lang and i18n[conf.lang] and i18n[conf.lang][key] then
- return i18n[conf.lang][key]
- else
- return i18n[conf["default_language"]][key] or ""
- end
-end
-
-
--- Store a message in the flash shared table in order to display it at the
--- next response
-function flash(wat, message)
- if wat == "fail"
- or wat == "win"
- or wat == "info"
- then
- flashs[wat] = message
- end
-end
-
-
--- Hash a string using hmac_sha512, return a hexa string
-function hmac_sha512(key, message)
- local cache_key = key..":"..message
-
- if not cache:get(cache_key) then
- -- lua ecosystem is a disaster and it was not possible to find a good
- -- easily multiplatform integrable code for this
- --
- -- this is really dirty and probably leak the key and the message in the process list
- -- but if someone got there I guess we really have other problems so this is acceptable
- -- and also this is way better than the previous situation
- local pipe = io.popen("echo -n '" ..message:gsub("'", "'\\''").. "' | openssl dgst -sha512 -hmac '" ..key:gsub("'", "'\\''").. "'")
-
- -- openssl returns something like this:
- -- root@yunohost:~# echo -n "qsd" | openssl sha512 -hmac "key"
- -- SHA2-512(stdin)= f1c2b1658fe64c5a3d16459f2f4eea213e4181905c190235b060ab2a4e7d6a41c15ea2c246828537a1e32ae524b7a7ed309e6d296089194c3e3e3efb98c1fbe3
- --
- -- so we need to remove the "SHA2-512(stdin)= " at the beginning ("(stdin)= " on older openssl version)
- local line = pipe:read()
- local hash = line:sub(line:find("=") + 2)
- pipe:close()
-
- cache:set(cache_key, hash, conf["session_timeout"])
- return hash
- else
- return cache:get(cache_key)
- end
-end
-
-
--- Convert a table of arguments to an URI string
-function uri_args_string(args)
- if not args then
- args = ngx.req.get_uri_args()
- end
- String = "?"
- for k,v in pairs(args) do
- String = String..tostring(k).."="..tostring(v).."&"
- end
- return string.sub(String, 1, string.len(String) - 1)
-end
-
-
--- Set the Cross-Domain-Authentication key for a specific user
-function set_cda_key()
- local cda_key = random_string()
- cache:set("CDA|"..cda_key, authUser, 10)
- return cda_key
-end
-
-
--- Compute and set the authentication cookie
---
--- Sets 3 cookies containing:
--- * The username
--- * The expiration time
--- * A hash of those information along with the client IP address and a unique
--- session key
---
--- It enables the SSO to quickly retrieve the username and the session
--- expiration time, and to prove their authenticity to avoid session hijacking.
---
-function set_auth_cookie(user, domain)
- local maxAge = conf["session_max_timeout"]
- local expire = ngx.req.start_time() + maxAge
- local session_key = cache:get("session_"..user)
- if not session_key then
- session_key = random_string()
- cache:add("session_"..user, session_key, conf["session_max_timeout"])
- end
- local hash = hmac_sha512(srvkey,
- user..
- "|"..expire..
- "|"..session_key)
- local cookie_str = "; Domain=."..domain..
- "; Path=/"..
- "; Expires="..ngx.cookie_time(expire)..
- "; Secure"..
- "; HttpOnly"..
- "; SameSite=Lax"
-
- ngx.header["Set-Cookie"] = {
- "SSOwAuthUser="..user..cookie_str,
- "SSOwAuthHash="..hash..cookie_str,
- "SSOwAuthExpire="..expire..cookie_str
- }
- logger.info("Hash "..hash.." generated for "..user.."@"..ngx.var.remote_addr)
-end
-
-
--- Expires the 3 session cookies
-function delete_cookie()
- for _, domain in ipairs(conf["domains"]) do
- local cookie_str = "; Domain=."..domain..
- "; Path=/"..
- "; Expires="..ngx.cookie_time(0)..
- "; Secure"..
- "; HttpOnly"..
- "; SameSite=Lax"
- ngx.header["Set-Cookie"] = {
- "SSOwAuthUser="..cookie_str,
- "SSOwAuthHash="..cookie_str,
- "SSOwAuthExpire="..cookie_str
- }
- end
-end
-
-
--- Validate authentification
---
--- Check if the session cookies are set, and rehash server + client information
--- to match the session hash.
---
-function refresh_logged_in()
- local expireTime = ngx.var.cookie_SSOwAuthExpire
- local user = ngx.var.cookie_SSOwAuthUser
- local authHash = ngx.var.cookie_SSOwAuthHash
-
- authUser = nil
- is_logged_in = false
-
- if expireTime and expireTime ~= ""
- and authHash and authHash ~= ""
- and user and user ~= ""
- then
- -- Check expire time
- if (ngx.req.start_time() <= tonumber(expireTime)) then
- -- Check hash
- local session_key = cache:get("session_"..user)
- if session_key and session_key ~= "" then
- -- Check cache
- if cache:get(user.."-password") then
- local hash = hmac_sha512(srvkey,
- user..
- "|"..expireTime..
- "|"..session_key)
- is_logged_in = hash == authHash
- if is_logged_in then
- authUser = user
- return true
- else
- failReason = "Hash not matching"
- end
- else
- failReason = "No {user}-password entry in cache"
- end
- else
- failReason = "No session key"
- end
- else
- failReason = "Cookie expired"
- end
- logger.debug("SSOwat cookies rejected for "..user.."@"..ngx.var.remote_addr.." : "..failReason)
- return false
- end
-
- return is_logged_in
-end
-
-function validate_or_clear_basic_auth_header_provided_by_client()
-
- -- Ignore if no Auth header
- local auth_header = ngx.req.get_headers()["Authorization"]
- if auth_header == nil then
- return nil
- end
-
- -- Ignore if not a Basic auth header
- _, _, b64_cred = string.find(auth_header, "^Basic%s+(.+)$")
- if b64_cred == nil then
- return nil
- end
-
- -- Try to authenticate the user,
- -- or remove the Auth header if not valid
- _, _, user, password = string.find(ngx.decode_base64(b64_cred), "^([^:]+):(.+)$")
- user = authenticate(user, password)
- if user then
- logger.debug("User got authenticated through basic auth")
- authUser = user
- return true
- else
- ngx.req.clear_header("Authorization")
- return false -- ngx.exit(ngx.HTTP_UNAUTHORIZED)
- end
-
-end
-
-
-
--- Check whether a user is allowed to access a URL using the `permissions` directive
--- of the configuration file
-function has_access(permission, user)
- user = user or authUser
-
- if permission == nil then
- logger.debug("No permission matching request for "..ngx.var.uri)
- return false
- end
-
- -- Public access
- if user == nil or permission["public"] then
- user = user or "A visitor"
- logger.debug(user.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
- return permission["public"]
- end
-
- logger.debug("User "..user.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
-
- -- The user has permission to access the content if he is in the list of allowed users
- if element_is_in_table(user, permission["users"]) then
- logger.debug("User "..user.." can access "..ngx.var.host..ngx.var.uri..uri_args_string())
- return true
- else
- logger.debug("User "..user.." cannot access "..ngx.var.uri)
- return false
- end
-end
-
-function element_is_in_table(element, table)
- if table then
- for _, el in pairs(table) do
- if el == element then
- return true
- end
- end
- end
-
- return false
-end
-
--- Authenticate a user against the LDAP database using a username or an email
--- address.
--- Reminder: conf["ldap_identifier"] is "uid" by default
-function authenticate(user, password)
- -- Try to find the username from an email address by openning an anonymous
- -- LDAP connection and check if the email address exists
- if conf["allow_mail_authentication"] and string.find(user, "@") then
- ldap = lualdap.open_simple(conf["ldap_host"])
- for dn, attribs in ldap:search {
- base = conf["ldap_group"],
- scope = "onelevel",
- sizelimit = 1,
- filter = "(mail="..user..")",
- attrs = {conf["ldap_identifier"]}
- } do
- if attribs[conf["ldap_identifier"]] then
- logger.debug("Use email: "..user)
- user = attribs[conf["ldap_identifier"]]
- else
- logger.error("Unknown email: "..user)
- return false
- end
- end
- ldap:close()
- end
-
- -- Now that we have a username, we can try connecting to the LDAP base.
- connected = lualdap.open_simple (
- conf["ldap_host"],
- conf["ldap_identifier"].."=".. user ..","..conf["ldap_group"],
- password
- )
-
- cache:flush_expired()
-
- -- If we are connected, we can retrieve the password and put it in the
- -- cache shared table in order to eventually reuse it later when updating
- -- profile information or just passing credentials to an application.
- if connected then
- if conf['ldap_enforce_crypt'] then
- ensure_user_password_uses_strong_hash(connected, user, password)
- end
- cache:add(user.."-password", password, conf["session_timeout"])
- ngx.log(ngx.NOTICE, "Connected as: "..user)
- logger.info("User "..user.." successfully authenticated from "..ngx.var.remote_addr)
- return user
-
- -- Else, the username/email or the password is wrong
- else
- -- N.B. : the ngx.log is important and is related to the regex used by
- -- the fail2ban rule to detect (and ban) failed login attempts
- ngx.log(ngx.ERR, "Connection failed for: "..user)
- logger.error("Authentication failure for user "..user.." from "..ngx.var.remote_addr)
- return false
- end
-end
-
-function delete_user_info_cache(user)
- cache:delete(user.."-"..conf["ldap_identifier"])
- local i = 2
- while cache:get(user.."-mail|"..i) do
- cache:delete(user.."-mail|"..i)
- i = i + 1
- end
- local i = 2
- while cache:get(user.."-maildrop|"..i) do
- cache:delete(user.."-maildrop|"..i)
- i = i + 1
- end
-end
-
--- Set the authentication headers in order to pass credentials to the
--- application underneath.
-function set_headers(user)
- local user = user or authUser
- -- Set `Authorization` header to enable HTTP authentification
- ngx.req.set_header("Authorization", "Basic "..ngx.encode_base64(
- user..":"..cache:get(user.."-password")
- ))
-
- -- Set optionnal additional headers (typically to pass email address)
- for k, v in pairs(conf["additional_headers"]) do
- ngx.req.set_header(k, cache:get(user.."-"..v))
- end
-
-end
-
--- Removes the authentication headers. Call me when:
--- - app is public and user is not authenticated
--- - app requests that no authentication headers be sent
--- Prevents user from pretending to be someone else on public apps
-function clear_headers()
- -- NB: Basic Auth header is cleared in validate_or_clear_basic_auth_header_provided_by_client
- for k, v in pairs(conf["additional_headers"]) do
- ngx.req.clear_header(k)
- end
-end
-
-function refresh_user_cache(user)
- -- We definitely don't want to pass credentials on a non-encrypted
- -- connection.
- if ngx.var.scheme ~= "https" then
- return redirect("https://"..ngx.var.host..ngx.var.uri..uri_args_string())
- end
-
- local user = user or authUser
-
- -- If the password is not in cache or if the cache has expired, ask for
- -- logging.
- if not cache:get(user.."-password") then
- flash("info", t("please_login"))
- local back_url = ngx.var.scheme .. "://" .. ngx.var.host .. ngx.var.uri .. uri_args_string()
- return redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url))
- end
-
- -- If the user information is not in cache, open an LDAP connection and
- -- fetch it.
- if not cache:get(user.."-"..conf["ldap_identifier"]) then
- ldap = lualdap.open_simple(
- conf["ldap_host"],
- conf["ldap_identifier"].."=".. user ..","..conf["ldap_group"],
- cache:get(user.."-password")
- )
- -- If the ldap connection fail (because the password was changed).
- -- Logout the user and invalid the password
- if not ldap then
- logger.debug("LDAP connection failed. Disconnect user : ".. user)
- cache:delete(authUser.."-password")
- flash("info", t("please_login"))
- local back_url = ngx.var.scheme .. "://" .. ngx.var.host .. ngx.var.uri .. uri_args_string()
- return redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url))
- end
- logger.debug("Reloading LDAP values for: "..user)
- for dn, attribs in ldap:search {
- base = conf["ldap_identifier"].."=".. user ..","..conf["ldap_group"],
- scope = "base",
- sizelimit = 1,
- attrs = conf["ldap_attributes"]
- } do
- for k,v in pairs(attribs) do
- if type(v) == "table" then
- for k2,v2 in ipairs(v) do
- if k2 == 1 then cache:set(user.."-"..k, v2, conf["session_timeout"]) end
- cache:set(user.."-"..k.."|"..k2, v2, conf["session_max_timeout"])
- end
- else
- cache:set(user.."-"..k, v, conf["session_timeout"])
- end
- end
- end
- else
- -- Else, just revalidate session for another day by default
- password = cache:get(user.."-password")
- -- Here we don't use set method to avoid strange logout
- -- See https://github.com/YunoHost/issues/issues/1830
- cache:replace(user.."-password", password, conf["session_timeout"])
- end
-end
-
-
--- Summarize email, aliases and forwards in a table for a specific user
-function get_mails(user)
- local mails = { mail = "", mailalias = {}, maildrop = {} }
-
- -- default mail
- mails["mail"] = cache:get(user.."-mail")
-
- -- mail aliases
- if cache:get(user.."-mail|2") then
- local i = 2
- while cache:get(user.."-mail|"..i) do
- table.insert(mails["mailalias"], cache:get(user.."-mail|"..i))
- i = i + 1
- end
- end
-
- -- mail forward
- if cache:get(user.."-maildrop|2") then
- local i = 2
- while cache:get(user.."-maildrop|"..i) do
- table.insert(mails["maildrop"], cache:get(user.."-maildrop|"..i))
- i = i + 1
- end
- end
- return mails
-end
-
-
--- Yo dawg, this enables SSOwat to serve files in HTTP in an HTTP server
--- Much reliable, very solid.
---
--- Takes an URI, and returns file content with the proper HTTP headers.
--- It is used to render the SSOwat portal *only*.
-function serve(uri, cache)
-
- logger.debug("Serving portal uri "..uri)
-
- rel_path = string.gsub(uri, conf["portal_path"], "/")
-
- -- Load login.html as index
- if rel_path == "/" then
- if is_logged_in then
- rel_path = "/portal.html"
- else
- rel_path = "/login.html"
- end
- end
-
- -- Access to directory root: forbidden
- if string.ends(rel_path, "/") then
- return ngx.exit(ngx.HTTP_FORBIDDEN)
- end
-
- -- Try to get file content
- local content = read_file(script_path.."portal"..rel_path)
- if not content then
- return ngx.exit(ngx.HTTP_NOT_FOUND)
- end
-
- -- Extract file extension
- _, file, ext = string.match(rel_path, "(.-)([^\\/]-%.?([^%.\\/]*))$")
-
- -- Associate to MIME type
- mime_types = {
- html = "text/html",
- ms = "text/html",
- js = "text/javascript",
- map = "text/javascript",
- css = "text/css",
- gif = "image/gif",
- jpg = "image/jpeg",
- png = "image/png",
- svg = "image/svg+xml",
- ico = "image/vnd.microsoft.icon",
- woff = "font/woff",
- woff2 = "font/woff2",
- ttf = "font/ttf",
- json = "application/json"
- }
-
- -- Allow .ms to specify mime type
- mime = ext
- if ext == "ms" then
- subext = string.match(file, "^.+%.(.+)%.ms$")
- if subext then
- mime = subext
- end
- end
-
- -- Set Content-Type
- if mime_types[mime] then
- ngx.header["Content-Type"] = mime_types[mime]
- else
- ngx.header["Content-Type"] = "text/plain"
- end
-
- -- Render as mustache
- if ext == "html" then
- local data = get_data_for(file)
- local rendered = lustache:render(read_file(script_path.."portal/header.ms"), data)
- rendered = rendered..lustache:render(content, data)
- content = rendered..lustache:render(read_file(script_path.."portal/footer.ms"), data)
- elseif ext == "ms" then
- local data = get_data_for(file)
- content = lustache:render(content, data)
- elseif uri == "/ynh_userinfo.json" then
- local data = get_data_for(file)
- content = json.encode(data)
- cache = "dynamic"
- end
-
- -- Reset flash messages
- flashs["fail"] = nil
- flashs["win"] = nil
- flashs["info"] = nil
-
- if cache == "static_asset" then
- ngx.header["Cache-Control"] = "public, max-age=3600"
- else
- -- Ain't nobody got time for cache
- ngx.header["Cache-Control"] = "no-cache"
- end
-
- -- Print file content
- ngx.say(content)
-
- -- Return 200 :-)
- return ngx.exit(ngx.HTTP_OK)
-end
-
-
--- Simple controller that computes a data table to populate a specific view.
--- The resulting data table typically contains the user information, the page
--- title, the flash notifications' content and the translated strings.
-function get_data_for(view)
- local user = authUser
-
- -- For the login page we only need the page title
- if view == "login.html" then
- data = {
- title = t("login"),
- connected = false
- }
-
- -- For those views, we may need user information
- elseif view == "portal.html"
- or view == "edit.html"
- or view == "password.html"
- or view == "ynh_userinfo.json" then
-
- -- Invalidate cache before loading these views.
- -- Needed if the LDAP db is changed outside ssowat (from the cli for example).
- -- Not doing it for ynhpanel.json only for performance reasons,
- -- so the panel could show wrong first name, last name or main email address
- -- TODO: What if we remove info during logout?
- --if view ~= "ynhpanel.json" then
- -- delete_user_info_cache(user)
- --end
-
- -- Be sure cache is loaded
- if user then
- refresh_user_cache(user)
-
- local mails = get_mails(user)
- data = {
- connected = true,
- theme = conf.theme,
- portal_url = conf.portal_url,
- uid = user,
- cn = cache:get(user.."-cn"),
- sn = cache:get(user.."-sn"),
- givenName = cache:get(user.."-givenName"),
- mail = mails["mail"],
- mailalias = mails["mailalias"],
- maildrop = mails["maildrop"],
- app = {}
- }
-
- local sorted_apps = {}
-
- -- Add user's accessible URLs using the ACLs.
- -- It is typically used to build the app list.
- for permission_name, permission in pairs(conf["permissions"]) do
- -- We want to display a tile, and uris is not empty
- if permission['show_tile'] and next(permission['uris']) ~= nil and element_is_in_table(user, permission["users"]) then
- url = permission['uris'][1]
- name = permission['label']
-
- if ngx.var.host == conf["local_portal_domain"] then
- url = string.gsub(url, conf["original_portal_domain"], conf["local_portal_domain"])
- end
-
- table.insert(sorted_apps, name)
- table.sort(sorted_apps)
- table.insert(data["app"], index_of(sorted_apps, name), { url = url, name = name })
- end
- end
- end
- end
-
- -- Pass all the translated strings to the view (to use with t_)
- for k, v in pairs(i18n[conf["default_language"]]) do
- data["t_"..k] = (i18n[conf.lang] and i18n[conf.lang][k]) or v
- end
-
- -- Pass flash notification content
- data['flash_fail'] = {flashs["fail"]}
- data['flash_win'] = {flashs["win"] }
- data['flash_info'] = {flashs["info"]}
- data['theme'] = conf["theme"]
-
- return data
-end
-
--- this function is launched after a successful login
--- it checked if the user password is stored using the most secure hashing
--- algorithm available
--- if it's not the case, it migrates the password to this new hash algorithm
-function ensure_user_password_uses_strong_hash(ldap, user, password)
- local current_hashed_password = nil
-
- for dn, attrs in ldap:search {
- base = conf['ldap_group'],
- scope = "onelevel",
- sizelimit = 1,
- filter = "("..conf['ldap_identifier'].."="..user..")",
- attrs = {"userPassword"}
- } do
- current_hashed_password = attrs["userPassword"]:sub(0, 10)
- end
-
- -- if the password is not hashed using sha-512, which is the strongest
- -- available hash rehash it using that
- -- Here "{CRYPT}" means "uses linux auth system"
- -- "6" means "uses sha-512", any lower number mean a less strong algo (1 == md5)
- if current_hashed_password:sub(0, 10) ~= "{CRYPT}$6$" then
- local dn = conf["ldap_identifier"].."="..user..","..conf["ldap_group"]
- local hashed_password = hash_password(password)
- ldap:modify(dn, {'=', userPassword = hashed_password })
- end
-end
-
--- Read result of a command after given it securely the password
-function secure_cmd_password(cmd, password, start)
- -- Check password validity
- local tmp_file = os.tmpname()
- local w_pwd = io.popen("("..cmd..") | tee -a "..tmp_file, 'w')
- w_pwd:write(password)
- -- This second write is just to validate the password question
- -- Do not remove
- w_pwd:write("")
- w_pwd:close()
- local r_pwd = io.open(tmp_file, 'r')
- text = r_pwd:read "*a"
-
- -- Remove the extra end line
- if text:sub(-1, -1) == "\n" then
- text = text:sub(1, -2)
- end
- r_pwd:close()
- os.remove(tmp_file)
- return text
-end
-
--- Compute the user modification POST request
--- It has to update cached information and edit the LDAP user entry
--- according to the changes detected.
-function edit_user()
- -- We need these calls since we are in a POST request
- ngx.req.read_body()
- local args = ngx.req.get_post_args()
-
- -- Ensure that user is logged in and has passed information
- -- before continuing.
- if is_logged_in and args
- then
-
- -- Set HTTP status to 201
- ngx.status = ngx.HTTP_CREATED
- local user = authUser
-
- -- In case of a password modification
- -- TODO: split this into a new function
- if string.ends(ngx.var.uri, "password.html") then
-
- -- Check current password against the cached one
- if args.currentpassword
- and args.currentpassword == cache:get(user.."-password")
- then
- -- and the new password against the confirmation field's content
- if args.newpassword == args.confirm then
- -- Check password validity
- local result_msg = secure_cmd_password("python3 /usr/lib/python3/dist-packages/yunohost/utils/password.py", args.newpassword)
- validation_error = true
- if result_msg == nil or result_msg == "" then
- validation_error = nil
- end
- if validation_error == nil then
-
- local dn = conf["ldap_identifier"].."="..user..","..conf["ldap_group"]
-
- -- Open the LDAP connection
- local ldap = lualdap.open_simple(conf["ldap_host"], dn, args.currentpassword)
-
- local password = hash_password(args.newpassword)
-
- -- Modify the LDAP information
- if ldap:modify(dn, {'=', userPassword = password }) then
- if validation == nil then
- flash("win", t("password_changed"))
- else
- flash("win", t(result_msg))
- end
-
- -- Reset the password cache
- cache:set(user.."-password", args.newpassword, conf["session_timeout"])
- return redirect(conf.portal_url.."portal.html")
- else
- flash("fail", t("password_changed_error"))
- end
- else
- flash("fail", t(result_msg))
- end
- else
- flash("fail", t("password_not_match"))
- end
- else
- flash("fail", t("wrong_current_password"))
- end
- return redirect(conf.portal_url.."password.html")
-
-
- -- In case of profile modification
- -- TODO: split this into a new function
- elseif string.ends(ngx.var.uri, "edit.html") then
-
- -- Check that needed arguments exist
- if args.givenName and args.sn and args.mail then
-
- -- Unstack mailaliases
- local mailalias = {}
- if args["mailalias[]"] then
- if type(args["mailalias[]"]) == "string" then
- args["mailalias[]"] = {args["mailalias[]"]}
- end
- mailalias = args["mailalias[]"]
- end
-
- -- Unstack mail forwards
- local maildrop = {}
- if args["maildrop[]"] then
- if type(args["maildrop[]"]) == "string" then
- args["maildrop[]"] = {args["maildrop[]"]}
- end
- maildrop = args["maildrop[]"]
- end
-
- -- Limit domains per user:
- -- This ensures that a user already has an email address or an
- -- aliases that ends with a specific domain to claim new aliases
- -- on this domain.
- --
- -- I.E. You need to have xxx@domain.org to claim a
- -- yyy@domain.org alias.
- --
- local domains = {}
- local ldap = lualdap.open_simple(conf["ldap_host"])
- for dn, attribs in ldap:search {
- base = conf["ldap_group"],
- scope = "onelevel",
- sizelimit = 1,
- filter = "(uid="..user..")",
- attrs = {"mail"}
- } do
- -- Construct proper emails array
- local mail_list = {}
- local mail_attr = attribs["mail"]
- if type(mail_attr) == "string" then
- mail_list = { mail_attr }
- elseif type(mail_attr) == "table" then
- mail_list = mail_attr
- end
-
- -- Filter configuration's domain list to keep only
- -- "allowed" domains
- for _, domain in ipairs(conf["domains"]) do
- for k, mail in ipairs(mail_list) do
- if string.ends(mail, "@"..domain) then
- if not is_in_table(domains, domain) then
- table.insert(domains, domain)
- end
- end
- end
- end
- end
- ldap:close()
-
- local rex = require "rex_pcre2"
- local rex_flags = rex.flags()
- local mail_re = rex.new([[^[\w\.\-+%]+@([^\W_A-Z]+([\-]*[^\W_A-Z]+)*\.)+([^\W\d_]{2,})$]], rex_flags.UTF8 + rex_flags.UCP)
-
- local mails = {}
-
- -- Build an LDAP filter so that we can ensure that email
- -- addresses are used only once.
- local filter = "(|"
- table.insert(mailalias, 1, args.mail)
-
- -- Loop through all the aliases
- for k, mail in ipairs(mailalias) do
- if mail ~= "" then
- -- Check the mail pattern
- if not mail_re:match(mail) then
- flash("fail", t("invalid_mail")..": "..mail)
- return redirect(conf.portal_url.."edit.html")
-
- -- Check that the domain is known and allowed
- else
- local domain_valid = false
- for _, domain in ipairs(domains) do
- if string.ends(mail, "@"..domain) then
- domain_valid = true
- break
- end
- end
- if domain_valid then
- table.insert(mails, mail)
- filter = filter.."(mail="..mail..")"
- else
- flash("fail", t("invalid_domain").." "..mail)
- return redirect(conf.portal_url.."edit.html")
- end
- end
- end
- end
-
- -- filter should look like "(|(mail=my@mail.tld)(mail=my@mail2.tld))"
- filter = filter..")"
-
-
- -- For email forwards, we only need to check that they look
- -- like actual emails
- local drops = {}
- for k, mail in ipairs(maildrop) do
- if mail ~= "" then
- if not mail_re:match(mail) then
- flash("fail", t("invalid_mailforward")..": "..mail)
- return redirect(conf.portal_url.."edit.html")
- end
- table.insert(drops, mail)
- end
- end
- table.insert(drops, 1, user)
-
-
- -- We now have a list of validated emails and forwards.
- -- We need to check if there is a user with a claimed email
- -- already before writing modifications to the LDAP.
- local dn = conf["ldap_identifier"].."="..user..","..conf["ldap_group"]
- local ldap = lualdap.open_simple(conf["ldap_host"], dn, cache:get(user.."-password"))
- local cn = args.givenName.." "..args.sn
-
- for dn, attribs in ldap:search {
- base = conf["ldap_group"],
- scope = "onelevel",
- filter = filter,
- attrs = {conf["ldap_identifier"], "mail"}
- } do
- -- Another user with one of these emails has been found.
- if attribs[conf["ldap_identifier"]] and attribs[conf["ldap_identifier"]] ~= user then
- -- Construct proper emails array
- local mail_list = {}
- local mail_attr = attribs["mail"]
- if type(mail_attr) == "string" then
- mail_list = { mail_attr }
- elseif type(mail_attr) == "table" then
- mail_list = mail_attr
- end
-
- for _, mail in ipairs(mail_list) do
- if is_in_table(mails, mail) then
- flash("fail", t("mail_already_used").." "..mail)
- end
- end
- return redirect(conf.portal_url.."edit.html")
- end
- end
-
- -- No problem so far, we can write modifications to the LDAP
- if ldap:modify(dn, {'=', cn = cn,
- displayName = cn,
- givenName = args.givenName,
- sn = args.sn,
- mail = mails,
- maildrop = drops })
- then
- delete_user_info_cache(user)
- -- Ugly trick to force cache reloading
- refresh_user_cache(user)
- flash("win", t("information_updated"))
- return redirect(conf.portal_url.."portal.html")
-
- else
- flash("fail", t("user_saving_fail"))
- end
- else
- flash("fail", t("missing_required_fields"))
- end
- return redirect(conf.portal_url.."edit.html")
- end
- end
-end
-
--- hash the user password using sha-512 and using {CRYPT} to uses linux auth system
--- because ldap doesn't support anything stronger than sha1
-function hash_password(password)
- local hashed_password = secure_cmd_password("mkpasswd --method=sha-512", password)
- hashed_password = "{CRYPT}"..hashed_password
- return hashed_password
-end
-
--- Compute the user login POST request
--- It authenticates the user against the LDAP base then redirects to the portal
-function login()
-
- -- We need these calls since we are in a POST request
- ngx.req.read_body()
- local args = ngx.req.get_post_args()
- local uri_args = ngx.req.get_uri_args()
-
- args.user = string.lower(args.user)
-
- local user = authenticate(args.user, args.password)
- if user then
- ngx.status = ngx.HTTP_CREATED
- set_auth_cookie(user, ngx.var.host)
- else
- ngx.status = ngx.HTTP_UNAUTHORIZED
- flash("fail", t("wrong_username_password"))
- end
-
- -- Forward the `r` URI argument if it exists to redirect
- -- the user properly after a successful login.
- if uri_args.r then
- return redirect(conf.portal_url.."?r="..uri_args.r)
- else
- return redirect(conf.portal_url)
- end
-end
-
-
--- Compute the user logout request
--- It deletes session cached information to invalidate client side cookie
--- information.
-function logout()
-
- -- We need this call since we are in a POST request
- local args = ngx.req.get_uri_args()
-
- -- Delete user cookie if logged in (that should always be the case)
- if is_logged_in then
- delete_cookie()
- cache:delete("session_"..authUser)
- cache:delete(authUser.."-"..conf["ldap_identifier"]) -- Ugly trick to reload cache
- cache:delete(authUser.."-password")
- delete_user_info_cache(authUser)
- flash("info", t("logged_out"))
- is_logged_in = false
- end
-
- -- Redirect with the `r` URI argument if it exists or redirect to portal
- if args.r then
- return redirect(ngx.decode_base64(args.r))
- else
- return redirect(conf.portal_url)
- end
-end
-
-
--- Set cookie and redirect (needed to properly set cookie)
-function redirect(url)
- logger.debug("Redirecting to "..url)
- -- For security reason we don't allow to redirect onto unknown domain
- -- And if `uri_args.r` contains line break, someone is probably trying to
- -- pass some additional headers
-
- -- This should cover the following cases:
- -- https://malicious.domain.tld/foo/bar
- -- http://malicious.domain.tld/foo/bar
- -- https://malicious.domain.tld:1234/foo
- -- malicious.domain.tld/foo/bar
- -- (/foo/bar, in which case no need to make sure it's prefixed with https://)
- if not string.starts(url, "/") and not string.starts(url, "http://") and not string.starts(url, "https://") then
- url = "https://"..url
- end
- local is_known_domain = string.starts(url, "/")
- for _, domain in ipairs(conf["domains"]) do
- if is_known_domain then
- break
- end
- -- Replace - character to %- because - is a special char for regex in lua
- domain = string.gsub(domain, "%-","%%-")
- is_known_domain = is_known_domain or url:match("^https?://"..domain.."/?") ~= nil
- end
- if string.match(url, "(.*)\n") or not is_known_domain then
- logger.debug("Unauthorized redirection to "..url)
- flash("fail", t("redirection_error_invalid_url"))
- url = conf.portal_url
- end
- return ngx.redirect(url)
-end
-
-
--- Set cookie and go on with the response (needed to properly set cookie)
-function pass()
- logger.debug("Allowing to pass through "..ngx.var.uri)
-
- -- When we are in the SSOwat portal, we need a default `content-type`
- if string.ends(ngx.var.uri, "/")
- or string.ends(ngx.var.uri, ".html")
- or string.ends(ngx.var.uri, ".htm")
- then
- ngx.header["Content-Type"] = "text/html"
- end
-
- return
-end
diff --git a/init.lua b/init.lua
index 6993e53..06a47f8 100644
--- a/init.lua
+++ b/init.lua
@@ -3,65 +3,52 @@
--
-- This is the initialization file of SSOwat. It is called once at the Nginx
-- server's start.
--- Consequently, all the variables declared (along with libraries and
+-- Consequently, all the variables declared (along with libraries and
-- translations) in this file will be *persistent* from one HTTP request to
-- another.
--
--- Path of the configuration
-conf_path = "/etc/ssowat/conf.json"
-log_file = "/var/log/nginx/ssowat.log"
-
-- Remove prepending '@' & trailing 'init.lua'
script_path = string.sub(debug.getinfo(1).source, 2, -9)
-
-- Include local libs in package.path
package.path = package.path .. ";"..script_path.."?.lua"
--- Load libraries
-local json = require "json"
-local lualdap = require "lualdap"
-local math = require "math"
-local lfs = require "lfs"
-local socket = require "socket"
-local config = require "config"
-lustache = require "lustache"
+-- Load cookie secret
+-- IMPORTANT (though to be confirmed?)
+-- in this context, the code is ran as root therefore we don't have to
+-- add www-data in the file permissions, which could otherwise lead
+-- to comprised apps running with the www-data group to read the secret file?
+local config = require("config")
+cookie_secret = config.get_cookie_secret()
+
+--
+-- Init logger
+--
+
+local log_file = "/var/log/nginx/ssowat.log"
-- Make sure the log file exists and we can write in it
io.popen("touch "..log_file)
io.popen("chown www-data "..log_file)
io.popen("chmod u+w "..log_file)
--- Persistent shared table
-flashs = {}
-i18n = {}
+local Logging = require("logging")
+local appender = function(self, level, message)
--- convert a string to a hex
-function tohex(str)
- return (str:gsub('.', function (c)
- return string.format('%02X', string.byte(c))
- end))
+ -- Output to log file
+ local fp = io.open(log_file, "a")
+ local str = string.format("[%-6s%s] %s\n", level:upper(), os.date(), message)
+ fp:write(str)
+ fp:close()
+
+ return true
end
--- Efficient function to get a random string
-function random_string()
- local length = 64
- local random_bytes = io.open("/dev/urandom"):read(length);
- if string.len(random_bytes) ~= length then
- error("Not enough random bytes read")
- end
- return tohex(random_bytes);
-end
+logger = Logging.new(appender)
+
+-- FIXME : how to set logging level ?
+--logger:setLevel(logger.DEBUG) -- FIXME
--- Load translations in the "i18n" above table
-local locale_dir = script_path.."portal/locales/"
-for file in lfs.dir(locale_dir) do
- if string.sub(file, -4) == "json" then
- local lang = string.sub(file, 1, 2)
- local locale_file = io.open(locale_dir..file, "r")
- i18n[lang] = json.decode(locale_file:read("*all"))
- end
-end
-- You should see that in your Nginx error logs by default
ngx.log(ngx.INFO, "SSOwat ready")
diff --git a/log.lua b/log.lua
deleted file mode 100644
index 9614a66..0000000
--- a/log.lua
+++ /dev/null
@@ -1,84 +0,0 @@
---
--- log.lua
---
--- Copyright (c) 2016 rxi
---
--- This library is free software; you can redistribute it and/or modify it
--- under the terms of the MIT license. See LICENSE for details.
---
-
-local log = { _version = "0.1.0" }
-local conf = config.get_config()
-
-log.usecolor = true
-log.level = conf.logging
-
-local modes = {
- { name = "trace", color = "\27[34m", },
- { name = "debug", color = "\27[36m", },
- { name = "info", color = "\27[32m", },
- { name = "warn", color = "\27[33m", },
- { name = "error", color = "\27[31m", },
- { name = "fatal", color = "\27[35m", },
-}
-
-
-local levels = {}
-for i, v in ipairs(modes) do
- levels[v.name] = i
-end
-
-
-local round = function(x, increment)
- increment = increment or 1
- x = x / increment
- return (x > 0 and math.floor(x + .5) or math.ceil(x - .5)) * increment
-end
-
-
-local _tostring = tostring
-
-local tostring = function(...)
- local t = {}
- for i = 1, select('#', ...) do
- local x = select(i, ...)
- if type(x) == "number" then
- x = round(x, .01)
- end
- t[#t + 1] = _tostring(x)
- end
- return table.concat(t, " ")
-end
-
-
-for i, x in ipairs(modes) do
- local nameupper = x.name:upper()
- log[x.name] = function(...)
-
- -- Return early if we're below the log level
- if i < levels[log.level] then
- return
- end
-
- local msg = tostring(...)
- local info = debug.getinfo(2, "Sl")
-
- -- Output to console
- print(string.format("%s[%-6s%s]%s %s",
- log.usecolor and x.color or "",
- nameupper,
- os.date("%H:%M:%S"),
- log.usecolor and "\27[0m" or "",
- msg))
-
- -- Output to log file
- local fp = io.open(log_file, "a")
- local str = string.format("[%-6s%s] %s\n",
- nameupper, os.date(), msg)
- fp:write(str)
- fp:close()
- end
-end
-
-
-return log
diff --git a/lustache.lua b/lustache.lua
deleted file mode 100644
index 94d8a25..0000000
--- a/lustache.lua
+++ /dev/null
@@ -1,29 +0,0 @@
--- lustache: Lua mustache template parsing.
--- Copyright 2013 Olivine Labs, LLC
--- MIT Licensed.
-
-module('lustache', package.seeall)
-
-local string_gmatch = string.gmatch
-
-function string.split(str, sep)
- local out = {}
- for m in string_gmatch(str, "[^"..sep.."]+") do out[#out+1] = m end
- return out
-end
-
-local lustache = {
- name = "lustache",
- version = "1.3.1-0",
- renderer = require("lustache.renderer"):new(),
-}
-
-return setmetatable(lustache, {
- __index = function(self, idx)
- if self.renderer[idx] then return self.renderer[idx] end
- end,
- __newindex = function(self, idx, val)
- if idx == "partials" then self.renderer.partials = val end
- if idx == "tags" then self.renderer.tags = val end
- end
-})
diff --git a/lustache/LICENSE b/lustache/LICENSE
deleted file mode 100644
index b16c8f8..0000000
--- a/lustache/LICENSE
+++ /dev/null
@@ -1,22 +0,0 @@
-The MIT License
-
-Copyright (c) 2012 Olivine Labs
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lustache/context.lua b/lustache/context.lua
deleted file mode 100644
index f8ec1a5..0000000
--- a/lustache/context.lua
+++ /dev/null
@@ -1,66 +0,0 @@
-local string_find, string_split, tostring, type =
- string.find, string.split, tostring, type
-
-local context = {}
-context.__index = context
-
-function context:clear_cache()
- self.cache = {}
-end
-
-function context:push(view)
- return self:new(view, self)
-end
-
-function context:lookup(name)
- local value = self.cache[name]
-
- if not value then
- if name == "." then
- value = self.view
- else
- local context = self
-
- while context do
- if string_find(name, ".") > 0 then
- local names = string_split(name, ".")
- local i = 0
-
- value = context.view
-
- if(type(value)) == "number" then
- value = tostring(value)
- end
-
- while value and i < #names do
- i = i + 1
- value = value[names[i]]
- end
- else
- value = context.view[name]
- end
-
- if value then
- break
- end
-
- context = context.parent
- end
- end
-
- self.cache[name] = value
- end
-
- return value
-end
-
-function context:new(view, parent)
- local out = {
- view = view,
- parent = parent,
- cache = {},
- }
- return setmetatable(out, context)
-end
-
-return context
diff --git a/lustache/renderer.lua b/lustache/renderer.lua
deleted file mode 100644
index 94e0cfb..0000000
--- a/lustache/renderer.lua
+++ /dev/null
@@ -1,388 +0,0 @@
-local Scanner = require "lustache.scanner"
-local Context = require "lustache.context"
-
-local error, ipairs, loadstring, pairs, setmetatable, tostring, type =
- error, ipairs, loadstring, pairs, setmetatable, tostring, type
-local math_floor, math_max, string_find, string_gsub, string_split, string_sub, table_concat, table_insert, table_remove =
- math.floor, math.max, string.find, string.gsub, string.split, string.sub, table.concat, table.insert, table.remove
-
-local patterns = {
- white = "%s*",
- space = "%s+",
- nonSpace = "%S",
- eq = "%s*=",
- curly = "%s*}",
- tag = "[#\\^/>{&=!]"
-}
-
-local html_escape_characters = {
- ["&"] = "&",
- ["<"] = "<",
- [">"] = ">",
- ['"'] = """,
- ["'"] = "'",
- ["/"] = "/"
-}
-
-local function is_array(array)
- if type(array) ~= "table" then return false end
- local max, n = 0, 0
- for k, _ in pairs(array) do
- if not (type(k) == "number" and k > 0 and math_floor(k) == k) then
- return false
- end
- max = math_max(max, k)
- n = n + 1
- end
- return n == max
-end
-
--- Low-level function that compiles the given `tokens` into a
--- function that accepts two arguments: a Context and a
--- Renderer.
-
-local function compile_tokens(tokens, originalTemplate)
- local subs = {}
-
- local function subrender(i, tokens)
- if not subs[i] then
- local fn = compile_tokens(tokens, originalTemplate)
- subs[i] = function(ctx, rnd) return fn(ctx, rnd) end
- end
- return subs[i]
- end
-
- local function render(ctx, rnd)
- local buf = {}
- local token, section
- for i, token in ipairs(tokens) do
- local t = token.type
- buf[#buf+1] =
- t == "#" and rnd:_section(
- token, ctx, subrender(i, token.tokens), originalTemplate
- ) or
- t == "^" and rnd:_inverted(
- token.value, ctx, subrender(i, token.tokens)
- ) or
- t == ">" and rnd:_partial(token.value, ctx, originalTemplate) or
- (t == "{" or t == "&") and rnd:_name(token.value, ctx, false) or
- t == "name" and rnd:_name(token.value, ctx, true) or
- t == "text" and token.value or ""
- end
- return table_concat(buf)
- end
- return render
-end
-
-local function escape_tags(tags)
-
- return {
- string_gsub(tags[1], "%%", "%%%%").."%s*",
- "%s*"..string_gsub(tags[2], "%%", "%%%%"),
- }
-end
-
-local function nest_tokens(tokens)
- local tree = {}
- local collector = tree
- local sections = {}
- local token, section
-
- for i,token in ipairs(tokens) do
- if token.type == "#" or token.type == "^" then
- token.tokens = {}
- sections[#sections+1] = token
- collector[#collector+1] = token
- collector = token.tokens
- elseif token.type == "/" then
- if #sections == 0 then
- error("Unopened section: "..token.value)
- end
-
- -- Make sure there are no open sections when we're done
- section = table_remove(sections, #sections)
-
- if not section.value == token.value then
- error("Unclosed section: "..section.value)
- end
-
- section.closingTagIndex = token.startIndex
-
- if #sections > 0 then
- collector = sections[#sections].tokens
- else
- collector = tree
- end
- else
- collector[#collector+1] = token
- end
- end
-
- section = table_remove(sections, #sections)
-
- if section then
- error("Unclosed section: "..section.value)
- end
-
- return tree
-end
-
--- Combines the values of consecutive text tokens in the given `tokens` array
--- to a single token.
-local function squash_tokens(tokens)
- local out, txt = {}, {}
- local txtStartIndex, txtEndIndex
- for _, v in ipairs(tokens) do
- if v.type == "text" then
- if #txt == 0 then
- txtStartIndex = v.startIndex
- end
- txt[#txt+1] = v.value
- txtEndIndex = v.endIndex
- else
- if #txt > 0 then
- out[#out+1] = { type = "text", value = table_concat(txt), startIndex = txtStartIndex, endIndex = txtEndIndex }
- txt = {}
- end
- out[#out+1] = v
- end
- end
- if #txt > 0 then
- out[#out+1] = { type = "text", value = table_concat(txt), startIndex = txtStartIndex, endIndex = txtEndIndex }
- end
- return out
-end
-
-local function make_context(view)
- if not view then return view end
- return getmetatable(view) == Context and view or Context:new(view)
-end
-
-local renderer = { }
-
-function renderer:clear_cache()
- self.cache = {}
- self.partial_cache = {}
-end
-
-function renderer:compile(tokens, tags, originalTemplate)
- tags = tags or self.tags
- if type(tokens) == "string" then
- tokens = self:parse(tokens, tags)
- end
-
- local fn = compile_tokens(tokens, originalTemplate)
-
- return function(view)
- return fn(make_context(view), self)
- end
-end
-
-function renderer:render(template, view, partials)
- if type(self) == "string" then
- error("Call mustache:render, not mustache.render!")
- end
-
- if partials then
- -- remember partial table
- -- used for runtime lookup & compile later on
- self.partials = partials
- end
-
- if not template then
- return ""
- end
-
- local fn = self.cache[template]
-
- if not fn then
- fn = self:compile(template, self.tags, template)
- self.cache[template] = fn
- end
-
- return fn(view)
-end
-
-function renderer:_section(token, context, callback, originalTemplate)
- local value = context:lookup(token.value)
-
- if type(value) == "table" then
- if is_array(value) then
- local buffer = ""
-
- for i,v in ipairs(value) do
- buffer = buffer .. callback(context:push(v), self)
- end
-
- return buffer
- end
-
- return callback(context:push(value), self)
- elseif type(value) == "function" then
- local section_text = string_sub(originalTemplate, token.endIndex+1, token.closingTagIndex - 1)
-
- local scoped_render = function(template)
- return self:render(template, context)
- end
-
- return value(section_text, scoped_render) or ""
- else
- if value then
- return callback(context, self)
- end
- end
-
- return ""
-end
-
-function renderer:_inverted(name, context, callback)
- local value = context:lookup(name)
-
- -- From the spec: inverted sections may render text once based on the
- -- inverse value of the key. That is, they will be rendered if the key
- -- doesn't exist, is false, or is an empty list.
-
- if value == nil or value == false or (type(value) == "table" and is_array(value) and #value == 0) then
- return callback(context, self)
- end
-
- return ""
-end
-
-function renderer:_partial(name, context, originalTemplate)
- local fn = self.partial_cache[name]
-
- -- check if partial cache exists
- if (not fn and self.partials) then
-
- local partial = self.partials[name]
- if (not partial) then
- return ""
- end
-
- -- compile partial and store result in cache
- fn = self:compile(partial, nil, originalTemplate)
- self.partial_cache[name] = fn
- end
- return fn and fn(context, self) or ""
-end
-
-function renderer:_name(name, context, escape)
- local value = context:lookup(name)
-
- if type(value) == "function" then
- value = value(context.view)
- end
-
- local str = value == nil and "" or value
- str = tostring(str)
-
- if escape then
- return string_gsub(str, '[&<>"\'/]', function(s) return html_escape_characters[s] end)
- end
-
- return str
-end
-
--- Breaks up the given `template` string into a tree of token objects. If
--- `tags` is given here it must be an array with two string values: the
--- opening and closing tags used in the template (e.g. ["<%", "%>"]). Of
--- course, the default is to use mustaches (i.e. Mustache.tags).
-function renderer:parse(template, tags)
- tags = tags or self.tags
- local tag_patterns = escape_tags(tags)
- local scanner = Scanner:new(template)
- local tokens = {} -- token buffer
- local spaces = {} -- indices of whitespace tokens on the current line
- local has_tag = false -- is there a {{tag} on the current line?
- local non_space = false -- is there a non-space char on the current line?
-
- -- Strips all whitespace tokens array for the current line if there was
- -- a {{#tag}} on it and otherwise only space
- local function strip_space()
- if has_tag and not non_space then
- while #spaces > 0 do
- table_remove(tokens, table_remove(spaces))
- end
- else
- spaces = {}
- end
- has_tag = false
- non_space = false
- end
-
- local type, value, chr
-
- while not scanner:eos() do
- local start = scanner.pos
-
- value = scanner:scan_until(tag_patterns[1])
-
- if value then
- for i = 1, #value do
- chr = string_sub(value,i,i)
-
- if string_find(chr, "%s+") then
- spaces[#spaces+1] = #tokens + 1
- else
- non_space = true
- end
-
- tokens[#tokens+1] = { type = "text", value = chr, startIndex = start, endIndex = start }
- start = start + 1
- if chr == "\n" then
- strip_space()
- end
- end
- end
-
- if not scanner:scan(tag_patterns[1]) then
- break
- end
-
- has_tag = true
- type = scanner:scan(patterns.tag) or "name"
-
- scanner:scan(patterns.white)
-
- if type == "=" then
- value = scanner:scan_until(patterns.eq)
- scanner:scan(patterns.eq)
- scanner:scan_until(tag_patterns[2])
- elseif type == "{" then
- local close_pattern = "%s*}"..tags[2]
- value = scanner:scan_until(close_pattern)
- scanner:scan(patterns.curly)
- scanner:scan_until(tag_patterns[2])
- else
- value = scanner:scan_until(tag_patterns[2])
- end
-
- if not scanner:scan(tag_patterns[2]) then
- error("Unclosed tag at " .. scanner.pos)
- end
-
- tokens[#tokens+1] = { type = type, value = value, startIndex = start, endIndex = scanner.pos - 1 }
- if type == "name" or type == "{" or type == "&" then
- non_space = true --> what does this do?
- end
-
- if type == "=" then
- tags = string_split(value, patterns.space)
- tag_patterns = escape_tags(tags)
- end
- end
-
- return nest_tokens(squash_tokens(tokens))
-end
-
-function renderer:new()
- local out = {
- cache = {},
- partial_cache = {},
- tags = {"{{", "}}"}
- }
- return setmetatable(out, { __index = self })
-end
-
-return renderer
diff --git a/lustache/scanner.lua b/lustache/scanner.lua
deleted file mode 100644
index 0673df1..0000000
--- a/lustache/scanner.lua
+++ /dev/null
@@ -1,57 +0,0 @@
-local string_find, string_match, string_sub =
- string.find, string.match, string.sub
-
-local scanner = {}
-
--- Returns `true` if the tail is empty (end of string).
-function scanner:eos()
- return self.tail == ""
-end
-
--- Tries to match the given regular expression at the current position.
--- Returns the matched text if it can match, `null` otherwise.
-function scanner:scan(pattern)
- local match = string_match(self.tail, pattern)
-
- if match and string_find(self.tail, pattern) == 1 then
- self.tail = string_sub(self.tail, #match + 1)
- self.pos = self.pos + #match
-
- return match
- end
-
-end
-
--- Skips all text until the given regular expression can be matched. Returns
--- the skipped string, which is the entire tail of this scanner if no match
--- can be made.
-function scanner:scan_until(pattern)
-
- local match
- local pos = string_find(self.tail, pattern)
-
- if pos == nil then
- match = self.tail
- self.pos = self.pos + #self.tail
- self.tail = ""
- elseif pos == 1 then
- match = nil
- else
- match = string_sub(self.tail, 1, pos - 1)
- self.tail = string_sub(self.tail, pos)
- self.pos = self.pos + #match
- end
-
- return match
-end
-
-function scanner:new(str)
- local out = {
- str = str,
- tail = str,
- pos = 1
- }
- return setmetatable(out, { __index = self } )
-end
-
-return scanner
diff --git a/portal/assets/css/ynh_overlay.css b/portal/assets/css/ynh_overlay.css
deleted file mode 100644
index 1604004..0000000
--- a/portal/assets/css/ynh_overlay.css
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
-===============================================================================
- This file contains CSS rules loaded on all apps page (*if* the app nginx's
- conf does include the appropriate snippet) for the small YunoHost button in
- bottom-right corner + portal overlay.
-
- The yunohost button corresponds to : #ynh-overlay-switch
- The yunohost portal overlay / iframe corresponds to : #ynh-overlay
-
- BE CAREFUL that you should *not* add too-general rules that apply to
- non-yunohost elements (for instance all 'a' or 'p' elements...) as it will
- likely break app's rendering
-===============================================================================
-*/
-
-/* ******************************************************************
- General
-******************************************************************* */
-
-html.ynh-panel-active {
- /* Disable any scrolling on app */
- overflow: hidden;
-
-}
-
-body {
- overflow-y: auto;
-}
-
-#ynh-overlay-switch,
-#ynh-overlay-switch *,
-#ynh-overlay,
-#ynh-overlay * {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-
-/* ******************************************************************
- Button
-******************************************************************* */
-#ynh-overlay-switch {
- display: block;
- position: fixed;
- z-index: 10000000;
- bottom: 20px;
- right: 35px;
- width: 100px;
- height: 90px;
- padding: 12px;
- border: 12px solid #41444f;
- border-radius: 5px;
- background: #41444f;
- background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE1LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluICAtLT4KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIiBbCgk8IUVOVElUWSBuc19mbG93cyAiaHR0cDovL25zLmFkb2JlLmNvbS9GbG93cy8xLjAvIj4KXT4KPHN2ZyB2ZXJzaW9uPSIxLjEiCgkgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6YT0iaHR0cDovL25zLmFkb2JlLmNvbS9BZG9iZVNWR1ZpZXdlckV4dGVuc2lvbnMvMy4wLyIKCSB4PSIwcHgiIHk9IjBweCIgd2lkdGg9Ijk4cHgiIGhlaWdodD0iODVweCIgdmlld0JveD0iLTAuMjUgLTAuMjUgOTggODUiCgkgb3ZlcmZsb3c9InZpc2libGUiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgLTAuMjUgLTAuMjUgOTggODUiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8ZGVmcz4KPC9kZWZzPgo8cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNOTcsNTFjLTIuMDIsNC45OC04LjMzLDUuNjctMTQsN2MtMC42MDksNi4yOSwzLjA1LDEwLjk1LTEsMTZjLTYuNDEtMC4yNi03LjQ3MS01Ljg1OS03LTEzYy0xLDAtMiwwLTMsMAoJYy0yLjA5LDIuNzcsMC45LDQuNTIsMCw4Yy0xLjEyLDQuMzQtNy44OCw3LjkxLTExLDdjLTIuMTgtMC42NDEtNS45Ni02LjYzLTUtMTJjMi44Mi0yLjcxLDIuNzYsMy4xMiw2LDNjNS4wNS03Ljg0LTkuNjMtOC41NS04LTE3CgljMS4yNC02LjQyLDExLjY2LTkuNjYsMTUtMWMxLjU0LDQuMjEtNS4xNywwLjE2LTUsM2MtMC4yNzksMS42MiwwLjk1LDEuNzIsMSwzYzIuNTIsMC43NywxLjY4LTIuMTYsMy0zYzEuODU5LTEuMTcsMy4wOS0wLjc1LDYtMQoJYzIuNDUtMi41NSwxLjA4LTguOTIsNC0xMWMzLjg3LDAuNDYsNi4wOCwyLjU5LDYsN0M5MS4wMSw0Ni4xMDksOTQuMyw0Ni4wNSw5Nyw1MXoiLz4KPHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTg3LDEzYzAuNjA5LDMuMjEsMi4zMiw0Ljk4LDIsOGMtMC4zNCwzLjIxLTIuOSw4LjgzLTQsOWMtMS4xNywwLjE4LTEuMzQsMS43OC0yLDIKCWMtNC42NiwxLjU3LTEyLjM5MS0xLjQ4LTE0LTdjLTEuMTYtMy45NywxLjktMTMuMzcsNC0xN2MxLjMtMi4yNSwxLjIyMS0yLjk5LDUtNGMyLjQxLTAuNjUsMy42NS0yLjI1LDYsMAoJYzAuNDcxLDAuNDUsMS4zLDAuNDksMS44NSwwLjg5Yy0wLjE5OSwwLDIsMy4xNCwyLjE1LDQuMTFDODguMzIsMTEuMDcsODYuNzcsMTEuNzgsODcsMTN6IE03OSwyMmMxLjc3OS0xLjg5LDMuMjktNC4wNCwzLTgKCUM3Ny40OSwxMi4zMyw3NC42NywyMS4zLDc5LDIyeiIvPgo8cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNNjcsMjFjLTAuMDcsNS44MSwyLjQ4LDEwLjcsMCwxNWMtNi43MywxLjA2LTcuMjQtNC4xLTExLTZjLTEuOTM5LDEuMzktMS40OSw1LjE4LTMsNwoJYy0zLjc4LDAuNDQtNC42OS0xLjk3LTctM2MyLjQ3LTcuODEsMS4yNi0xOC45OCwyLTI2YzguNTgtMC41OCw3LjY4LDguMzIsMTIsMTJjMC41Mi00LjM0LTAuMzU5LTE1LjUyLDMtMjAKCUM3MC4zMywzLjI5LDY3LjA5LDEyLjk5LDY3LDIxeiIvPgo8cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNNTIsNTVjMS45Myw4LjQxLDAuMTIsMjIuNjg5LTEyLDIwYy0xLjU5LTAuMzUtOC40Mi01LjIyLTktN2MtMS42Mi01LDAuMzQtMTMuMzQsMy0xNgoJQzM5LjAzLDQ2Ljk3LDQ1LjQ4LDUwLjM1OSw1Miw1NXogTTM5LDY2YzQuNTUsMC45Niw2LjMtNC4yLDQtN0MzOS4zNyw1OS4wMywzOC42MSw2MS45MzksMzksNjZ6Ii8+CjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0zOSw4YzUuNTgsMC45LDYuNCw2LjgxLDUsMTVjLTEuNDMsOC4zOC0zLjAyLDE0LjU5LTksMTVjLTkuNTcsMC42NS0xMi4yNS0xNi42OS05LTI5CgljOC4zMiwxLjI3LDYuNTksMTAuMzYsNiwxN2MyLjcxLDAuODMsMi4yLTAuODUsMy0yQzM3LjA1LDIxLjA0LDM3LjgyLDEzLjYxLDM5LDh6Ii8+CjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0yOCw2MmMwLjEsNS42Nyw0LjQsMTEuMzMsMiwxN2MtNC4zMi0xLjAxLTYuNTctNC4wOS05LTdjLTMuMTUtMC40OC0yLjI2LDMuMDctNiwyCgljLTAuNjcsNS4wNjEsMi4yOSw3LjU3LTEsMTBjLTQuNy0wLjYzLTYuNjYtNC04LThjLTIuNjEtMS4zOC01LjQ4LTIuNTItNi02YzAuMTQtMy41Myw0LjQ4LTIuODUsNy00YzAuNDctNS41My0xLjQxLTEzLjQxLDItMTYKCWM4LjMxLDAuNDksOC4yMSw3LjEzLDcsMTVjNC4zNiwwLjI5LDQuOTQtNC4zNSw1LTdjMC4wNi0yLjQzLTEuODItOC4yNiwyLTExYzMuMDYtMC43MywyLjk0LDEuNzMsNiwxCglDMzIuMzUsNTIuNywyNy45Miw1Ny40MzksMjgsNjJ6Ii8+CjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0yNCwxMmMxLjA3LDcuMDctMy44Niw4LjE0LTYsMTJjMC4yMSw2Ljg4LTAuNDcsMTIuODYtMiwxOGMtNS44Ni0xLjMyLTguNy0xMC4zOC02LTE3CgljLTAuMzMtMy41Mi01LjI2LTQuMjItNy04Yy0wLjMtMC42Ni0wLjQ3LTQuNDMtMS03QzEuMDksNS42MywwLjU1LDQuMzEsMywxYzguMTYtMC40OSw3LjIxLDguMTMsOSwxNGM1LjA1LDAuMzksMy45MS01LjQyLDgtNgoJQzIwLjk4LDEwLjM1LDIyLjY3LDExLDI0LDEyeiIvPgo8L3N2Zz4K);
- background-position: center center;
- background-repeat: no-repeat;
- background-size: contain;
- opacity: 0.7;
-}
-/*#ynh-overlay-switch.visible,*/
-#ynh-overlay-switch:hover {
- background-color: #41444f;
- border-color: #41444f;
- background-color: #111;
- border-color: #111;
-}
-
-
-/* ******************************************************************
- Overlay
-******************************************************************* */
-
-/* Background */
-#ynh-overlay {
- overflow-y: hidden;
- position: fixed;
- top:0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 9999999;
- display: none;
- border: none;
- color:#fff;
- background: #41444F;
- transition: all 0.2s ease;
- -moz-transition: all 0.2s ease;
- -webkit-transition: all 0.2s ease;
-}
-
-
-/* ******************************************************************
- Animation
-******************************************************************* */
-
-/*FadeIn*/
-@-webkit-keyframes ynhFadeIn {
- 0% {
- visibility: hidden;
- opacity:0;
- }
- 100% {
- visibility: visible;
- opacity: 1;
- }
-}
-@keyframes ynhFadeIn {
- 0% {
- visibility: hidden;
- opacity: 0;
- }
- 100% {
- visibility: visible;
- opacity: 1;
- }
-}
-
-.ynh-fadeIn {
- -webkit-animation-name: ynhFadeIn;
- animation-name: ynhFadeIn;
- -webkit-animation-duration: 0.5s;
- animation-duration: 0.5s;
- -webkit-animation-fill-mode: both;
- animation-fill-mode: both;
- -webkit-animation-timing-function: cubic-bezier(0.165, 0.840, 0.440, 1.000);
- animation-timing-function: cubic-bezier(0.165, 0.840, 0.440, 1.000);
-}
-/*
-.ynh-fadeIn.ynh-delay {
- animation-delay: 0.5s;
- -webkit-animation-delay: 0.5s;
-}
-*/
-
-/*FadeOut*/
-@-webkit-keyframes ynhFadeOut {
- 0% {
- visibility: visible;
- opacity: 1;
- }
- 100% {
- visibility: hidden;
- opacity: 0;
- }
-}
-@keyframes ynhFadeOut {
- 0% {
- visibility: visible;
- opacity: 1;
- }
- 100% {
- visibility: hidden;
- opacity: 0;
- }
-}
-.ynh-fadeOut {
- -webkit-animation-name: ynhFadeOut;
- animation-name: ynhFadeOut;
- -webkit-animation-duration: 0.2s;
- animation-duration: 0.2s;
- -webkit-animation-fill-mode: both;
- animation-fill-mode: both;
-}
-/*
-.ynh-fadeOut.ynh-delay {
- animation-delay: 0.5s;
- -webkit-animation-delay: 0.5s;
-}
-*/
-
-
-/* ******************************************************************
- Media Queries
-******************************************************************* */
-
-@media screen and (max-width: 500px) {
- #ynh-overlay-switch {
- width: 80px;
- height: 75px;
- }
-}
\ No newline at end of file
diff --git a/portal/assets/css/ynh_portal.css b/portal/assets/css/ynh_portal.css
deleted file mode 100644
index 4a580a6..0000000
--- a/portal/assets/css/ynh_portal.css
+++ /dev/null
@@ -1,828 +0,0 @@
-/*
-===============================================================================
- This file contain CSS rules loaded on the YunoHost user portal.
-===============================================================================
-*/
-
-/* ==========================================================================
- 0 = Fonts
- 1 = Global
- 2 = Apps
- 3 = User
- 4 = Form
- 5 = Footer
- 6 = Colors
- 7 = Internet Explorer
- ========================================================================== */
-
-/* ==========================================================================
- 0 = Fonts
- ========================================================================== */
-@font-face {
- font-family: 'ynh_ssowat';
- src: url('../fonts/ynh_ssowat/ynh_ssowat.eot');
-}
-
-@font-face {
- font-family: 'ynh_ssowat';
- src: url(data:application/x-font-ttf;charset=utf-8;base64,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) format('truetype'),
- url(data:application/font-woff;charset=utf-8;base64,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) format('woff');
- font-weight: normal;
- font-style: normal;
-}
-
-@font-face {
- font-family: 'source_sans_probold';
- src: url('../fonts/sourcesanspro-bold-webfont.eot');
- }
-
-@font-face {
- font-family: 'source_sans_probold';
- src: url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'),
- url('../fonts/sourcesanspro-bold-webfont.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-
-@font-face {
- font-family: 'source_sans_proregular';
- src: url('../fonts/sourcesanspro-regular-webfont.eot');
- }
-
-@font-face {
- font-family: 'source_sans_proregular';
- src: url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'),
- url('../fonts/sourcesanspro-regular-webfont.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-
-/* ==========================================================================
- 1 = Global
- ========================================================================== */
-
-* {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-html {
- font-family: sans-serif; /* 1 */
- -ms-text-size-adjust: 100%; /* 2 */
- -webkit-text-size-adjust: 100%; /* 2 */
- height: 100%;
-}
-
-body {
- background: #41444f;
- font-family: 'source_sans_proregular';
- font-size: 1em;
- line-height:1.5;
- margin: 0;
- padding: 0;
- height: 100%;
- top: 0;
- position: absolute;
- width: 100%;
-}
-
-h1:first-child,
-h2:first-child,
-h3:first-child,
-h4:first-child,
-h5:first-child,
-h6:first-child,
-p:first-child,
-ul:first-child,
-ol:first-child,
-dl:first-child{
- margin-top: 0;
-}
-
-
-
-
-/* Responsive image */
-img {
- max-width: 100%;
- height :auto;
-}
-
-.element-invisible {
- border: 0;
- clip: rect(0 0 0 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- width: 1px;
-}
-
-
-/* Layout */
-.content {
- padding: 2%;
-}
-
-.logged .content {
- width: 100%;
- height: 100%;
- overflow: auto; /* scroll .content, not body for easier background customization */
-}
-
-.ynh-wrapper {
- width: 90%;
- margin: 2% 5%;
- position: relative;
- z-index: 1;
-}
-.ynh-wrapper:before,
-.ynh-wrapper:after {content: " ";display: table;}
-.ynh-wrapper:after {clear: both;}
-
-/* Logo */
-.ynh-logo {
- opacity: 0.7;
- margin-top: 6em;
- width: 100%;
- height: 9em;
- background-image: url("../img/logo-ynh-white.svg");
- background-repeat: no-repeat;
- background-position: center 100%;
- background-size: contain;
-}
-
-.logged .ynh-logo {
- position: fixed;
- width: 5em;
- height: 5em;
- bottom: 20px;
- right: 20px;
- z-index: 0;
- opacity: 0.7;
- background-position: center center;
-}
-
-.ynh-panel-active .ynh-logo {
- display: none;
-}
-
-.in_app_overlay .ynh-logo {
- display: none;
-}
-
-/* messages */
-.messages {
- color: #FFF;
- margin-bottom: 1em;
- text-align: center;
- max-width: 21em;
- margin: 2% auto 1em auto;
- padding: 1.5em;
-}
-.messages.danger { background: #c0392b; }
-.messages.warning { background: #e67e22; }
-.messages.success { background: #27ae60; }
-.messages.info { background: #2980b9; }
-
-.logged .messages {
- max-width: none;
- margin: 2% 5%;
- padding: 1.5em 15%;
-}
-
-
-/* Fonts & Colors */
-
-a { text-decoration: none; }
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- font-family: 'source_sans_probold';
- font-weight: normal;
-}
-
-/* headings */
-h1,
-.h1 {
- font-size: 2.5em;
-}
-
-h2,
-.h2 {
- font-size: 1.8em;
-}
-
-.cwhite {
- color: #fff;
-}
-
-select,
-.form-text,
-textarea {
- border: 0;
- font-family: 'source_sans_proregular';
-}
-
-
-/* Icons */
-
-[class^="icon-"]:before, [class*=" icon-"]:before {
- font-family: 'ynh_ssowat';
- font-size: 1em;
- speak: none;
- font-style: normal;
- font-weight: normal;
- font-variant: normal;
- text-transform: none;
- line-height: 1;
- margin-right: 0.5em;
-
- /* Better Font Rendering =========== */
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.icon-user:before { content: '\e801'; }
-.icon-lock:before { content: '\e800'; }
-.icon-connexion:before { content: '\e802'; }
-.icon-pencil:before { content: '\e804'; }
-.icon-trash:before { content: '\e80c'; }
-.icon-angle-left:before { content: '\e803'; }
-
-
-/* ==========================================================================
- 2 = Apps
- ========================================================================== */
-
-.apps { margin: 4% 5%; }
-
-.listing-apps {
- margin: 0;
- padding: 0;
- letter-spacing: -5px; /*fix bug ff PC*/
- font-family: 'source_sans_probold';
-}
-
-.listing-apps li {
- display: inline-block;
- vertical-align: top;
- letter-spacing: normal;
- list-style: none;
- margin: 0 0 1em 1em;
- box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.4),
- -2px -2px 3px 0 rgba(0, 0, 0, 0.7) inset;
-}
-
-.listing-apps a {
- display: block;
- position: relative;
- padding: 0.2em;
- top: 0;
- left: 0;
- width: 2.7em;
- height: 2.7em;
- background: #666;
- color: #fff;
- font-size: 4em;
- transition: all 0.3s ease;
- -webkit-transition: all 0.3s ease;
-}
- .listing-apps a:hover,
- .listing-apps a:focus {
- left: -10px;
- top: -10px;
- box-shadow: none;
- }
- .listing-apps a:hover:before,
- .listing-apps a:focus:before {
- height: 10px;
- }
- .listing-apps a:hover:after,
- .listing-apps a:focus:after {
- width: 10px;
- }
-
- .listing-apps a:hover:after,
- .listing-apps a:focus:after,
- .listing-apps a:hover:before,
- .listing-apps a:focus:before {
- background: #333;
- }
-
- .listing-apps a:after,
- .listing-apps a:before {
- content: "";
- position: absolute;
- transition: all 0.3s ease;
- -webkit-transition: all 0.3s ease;
- }
-
- .listing-apps a:before {
- width: 100%;
- height: 0;
- left: 5px;
- top: 100%;
- box-shadow: 0 5px 10px rgba(0, 0, 0, 0.4);
- transform: skew(45deg, 0deg);
- -webkit-transform: skew(45deg, 0deg);
- }
-
- .listing-apps a:after {
- width: 0;
- height: 100%;
- left: 100%;
- top: 5px;
- box-shadow: 5px 0 10px rgba(0, 0, 0, 0.4);
- transform: skew(0deg, 45deg);
- -webkit-transform: skew(0deg, 45deg);
- }
-
- .listing-apps span {
- display: block;
- margin: -1.2em 0 0 0.2em;
- }
- .listing-apps .first-letter {
- margin: 0;
- display: inline-block;
- }
- .listing-apps .name {
- font-family: 'source_sans_proregular';
- font-size: 0.3em;
- }
-
-@media screen and (max-width: 450px) {
- .apps {margin: 10% 5%;}
- .listing-apps a {font-size: 3em;}
- .listing-apps span + span { font-size: 0.32em; }
- .listing-apps a:hover,
- .listing-apps a:focus {left: -5px;top: -5px;}
- .listing-apps a:hover:before,
- .listing-apps a:focus:before {height: 5px;}
- .listing-apps a:hover:after,
- .listing-apps a:focus:after {width: 5px;}
- .listing-apps a:before {left: 3px;box-shadow: 0 3px 5px rgba(0, 0, 0, 0.4);}
- .listing-apps a:after {top: 3px;box-shadow: 3px 0 5px rgba(0, 0, 0, 0.4);}
-}
-@media screen and (max-width: 350px) {
- .listing-apps a {
- width: 2.5em;
- height: 2.5em;
- font-size: 2.8em;
- }
-}
-
-
-/* ==========================================================================
- 3 = User
- ========================================================================== */
-
-.user-container {
- display:block;
- position: relative;
- max-width: 320px;
- padding: 0.4em 1em;
- color: #fff;
-}
-
-.user-container-edit:after,
-.user-container-password:after {
- content: '\e803';
- font-family: 'ynh_ssowat';
- display: block;
- height: 1em;
- width: 1em;
- position: absolute;
- top: 50%;
- left: -16px;
- z-index: 0;
- margin-top: -0.75em;
- font-size: 2em;
- font-weight: normal;
- color: #b4b4b4;
- opacity: 0;
- transition: all 0.1s ease;
- -webkit-transition: all 0.1s ease;
-}
-.user-container-edit:hover:after,
-.user-container-password:hover:after {
- left: -20px;
- opacity: 1;
-}
-@media screen and (max-width: 480px) {
- .user-container-edit:after,
- .user-container-password:after {left: -10px;}
- .user-container-edit:hover:after,
- .user-container-password:hover:after {left: -14px;}
-}
-
-.user-container:before {
- display: block;
- position: relative;
- z-index: 1;
- float: left;
- margin-right: 10px;
- content: '\e801';
- display: block;
- font-family: 'ynh_ssowat';
- font-size: 4em;
- text-align: center;
- border: 3px solid #fff;
- width: 1em;
- height: 1em;
- border-radius: 90px;
- background: #b4b4b4;
- color: #dedede;
- overflow: hidden;
- transition: all 0.1s ease;
- -webkit-transition: all 0.1s ease;
-}
-.user-container:hover:before {
- color: #fff;
-}
-
-.user-container .user-username {
- font-size: 1.5em;
- margin: 0;
-}
-
-.user-container .user-fullname {
- font-size: 1em;
- font-family: 'source_sans_proregular';
- display: block;
- margin-top: -0.6em;
-}
-
-
-.user-container-info .user-username:after {
- content: '\e804';
- font-family: 'ynh_ssowat';
- color: #b4b4b4;
- display: inline-block;vertical-align: text-top;
- font-size: 0.8em;
- width: 1em;
- height: 1em;
- margin-left: .5em;
- opacity: 0;
- transition: all 0.1s ease;
- -webkit-transition: all 0.1s ease;
-}
-.user-container-info:hover .user-username:after {opacity: 1;}
-
-.user-container .user-mail {
- color: #999;
- font-size: 0.9em;
- display: block;
- margin-top: -0.2em;
-}
-
-/* User menu */
-.user-menu {
- float: right;
- margin: 0;
- padding: 0;
-}
- .user-menu li {
- list-style: none;
- }
- .user-menu a {
- color: #999;
- display: block;
- padding: 1.25em 1em;
- position: relative;
- z-index: 1;
- transition: all 0.1s ease;
- -webkit-transition: all 0.1s ease;
- }
- .user-menu a:hover,
- .user-menu a:focus {
- color: #fff;
- }
-
-@media screen and (max-width: 480px) {
- .user-menu {
- float: none;
- }
-}
-
-
-
-/* ==========================================================================
- 4 = Forms
- ========================================================================== */
-
-button,
-input,
-select,
-textarea {
- font-family: inherit;
- font-size: 100%;
- margin: 0;
-}
-
-input[type="search"] {
- -webkit-appearance: textfield;
-}
-[type="submit"],
-[type="password"],
-[type="email"],
-[type="text"] {
- /* <3 Apple */
- -webkit-appearance: none;
- -webkit-border-radius:0;
-}
-
-input::-moz-focus-inner {
- border: 0;
- padding: 0;
-}
-
-@media screen and (-webkit-min-device-pixel-ratio:0){
- select{
- -webkit-appearance: none;
- border-radius: 0;
- }
-}
-
-.form-text {
- padding: 0.8em;
- width: 100%;
-}
-
-
-.form-section {
- display: inline-block;
- vertical-align: top;
- width: 47%;
-}
-.form-section + .form-section {margin-left: 5%;}
-
-@media screen and (max-width: 768px) {
- .form-section {width: 100%;}
- .form-section + .form-section {margin-left: 0;}
-}
-
-label {
- display: inline-block;
- padding: 0.3em 1em;
- background: #30333b;
- color: #fff;
- font-size: 1.2em;
- margin-top: 1em;
- font-family: 'source_sans_probold';
- font-weight: normal;
-}
-label {cursor: pointer;}
-
-label + .help-link {
- display: inline-block;
- padding: 0.3em 1em;
- font-size: 1.2em;
- background: #41444f;
- color: #fff;
- font-weight: bold;
- transition: all 0.1s ease;
- -webkit-transition: all 0.1s ease;
-}
-label + .help-link:hover {background: #30333b;}
-
-.form-group {
- background: none;
- margin-bottom: 2em;
-}
-
-.form-text {
- border: 0;
- background: #797b83;
- color: #fff;
- padding: 0.8em;
- margin-bottom: 0.3em;
- display: block;
- position: relative;z-index: 1; /* prevent strange label overlap */
- transition: all 0.1s ease;
- -webkit-transition: all 0.1s ease;
-}
-.form-test:-moz-placeholder{color:#ccc;}
-.form-text::-moz-placeholder{color:#ccc;}
-.form-text:-ms-input-placeholder{color:#ccc;}
-.form-text::-webkit-input-placeholder{color:#ccc;}
-:empty:invalid {box-shadow: none;}
-
-.form-text:last-child {margin-bottom:0;}
-
-.form-text:hover,
-.form-text:focus {
- background-color: #5d5f68;
-}
-
-.form-text:disabled { color: #ccc; }
-.form-text:disabled:hover {background-color:rgba(255, 255, 255, 0.3);}
-input:disabled {
- cursor: not-allowed;
-}
-
-@media screen and (max-width: 480px) {
- label,
- label + .help-link {padding: 0.3em 0.8em;}
- .form-text {padding: 0.8em;}
- .form-group .btn {padding: 0.5em 0.8em;}
-}
-
-
-
-/* Buttons */
-.btn {
- background: #999;
- display: inline-block;
- padding: 0.5em 1em;
- line-height: normal;
- text-decoration: none;
- color: #FFF;
- cursor: pointer;
- transition: all 0.1s ease;
- -webkit-transition: all 0.1s ease;
-}
-
-.large-btn {
- padding: 0.8em 1.5em;
- font-size: 1.1em;
-}
-
-button.btn,
-input.btn {
- border:0;
- cursor:pointer;
-}
-
- .btn:hover,
- .btn:focus {
- background: #AAA;
- }
-
-.important-btn { background: #c0392b;}
- .important-btn:hover,
- .important-btn:focus {background: #e74c3c;}
-
-.validate-btn { background: #27ae60;}
- .validate-btn:hover,
- .validate-btn:focus {background: #2ecc71;}
-
-.warning-btn { background: #e67e22;}
- .warning-btn:hover,
- .warning-btn:focus {background: #f39c12;}
-
-.classic-btn { background: #2980b9;}
- .classic-btn:hover,
- .classic-btn:focus {background: #3498db;}
-
-.link-btn { background: none;}
- .link-btn:hover,
- .link-btn:focus {background: #41444f;text-decoration: underline;}
-
-
-.btn-group {
- margin: 4em 0;
- text-align: right;
-}
-
-/* Login form */
-
-.login-form {
- max-width: 21em;
- margin: 0 auto;
-}
-
-.login-form .btn {
- width: 100%;
- padding: 0.8em 1em;
-}
-
-.login-form .form-group {
- position: relative;
- margin-bottom: 1em;
- background: #fff;
-}
-
- .login-form label {
- display: block;
- min-width: 1em;
- margin: 0;
- padding: 0;
- font-size: 1em;
- }
-
- .login-form label:before {
- background: #eee;
- color: #666;
- position: absolute;
- z-index: 2;
- top: 0;
- left: 0;
- width: 2.5em;
- height: 100%;
- line-height: 3em;
- text-align: center;
- }
-
- .login-form .form-text {
- padding: 0.8em 0.8em 0.8em 3em;
- width: 100%;
- background: #fff;
- color: #41444f;
- }
- .login-form .form-test:-moz-placeholder{color:#999;}
- .login-form .form-text::-moz-placeholder{color:#999;}
- .login-form .form-text:-ms-input-placeholder{color:#999;}
- .login-form .form-text::-webkit-input-placeholder{color:#999;}
-
-
-/* Edit form*/
-
-.form-edit .form-group .btn:before {
- content:"+";
- display: inline-block;
- padding-right: 0.75em;
- font-weight: bold;
-}
-
-
-@media screen and (min-width: 768px) {
- .form-edit .btn-group {
- float: right;
- }
- .form-edit .btn-group + .btn-group {
- float: left;
- }
-}
-
-
-/* ==========================================================================
- 5 = Footer
- ========================================================================== */
-
-.footer {
- display: inline-block;
- width: auto;
-}
- .footer nav {
- margin: 0 1em;
- padding: 0.25em;
- border-top: 1px solid #666;
- font-size: 0.9em;
- }
-
- .footer a {
- display: inline-block;
- vertical-align: top;
- color: #999;
- }
- .footer a:before {
- content: "•";
- display: inline-block;
- vertical-align: top;
- padding: 0 0.5em 0 0.25em;
- color: #666;
- }
- .footer a:first-child:before {content: none;}
-
- .footer a:hover,
- .footer a:active {
- color: #fff;
- text-decoration: none;
- }
-
-@media screen and (max-width: 480px) {
- .footer a {
- display: block;
- }
- .footer a:before {
- content: none;
- }
-}
-
-/* ==========================================================================
- Internet Explorer
- ========================================================================== */
-
-/*IE8 and IE9*/
-
-article,
-aside,
-details,
-figcaption,
-figure,
-footer,
-header,
-hgroup,
-main,
-nav,
-section,
-summary {
- display: block;
-}
diff --git a/portal/assets/fonts/sourcesanspro-bold-webfont.eot b/portal/assets/fonts/sourcesanspro-bold-webfont.eot
deleted file mode 100755
index bbbe8ee..0000000
Binary files a/portal/assets/fonts/sourcesanspro-bold-webfont.eot and /dev/null differ
diff --git a/portal/assets/fonts/sourcesanspro-bold-webfont.ttf b/portal/assets/fonts/sourcesanspro-bold-webfont.ttf
deleted file mode 100755
index 8ced94f..0000000
Binary files a/portal/assets/fonts/sourcesanspro-bold-webfont.ttf and /dev/null differ
diff --git a/portal/assets/fonts/sourcesanspro-regular-webfont.eot b/portal/assets/fonts/sourcesanspro-regular-webfont.eot
deleted file mode 100755
index 850509c..0000000
Binary files a/portal/assets/fonts/sourcesanspro-regular-webfont.eot and /dev/null differ
diff --git a/portal/assets/fonts/sourcesanspro-regular-webfont.ttf b/portal/assets/fonts/sourcesanspro-regular-webfont.ttf
deleted file mode 100755
index b9d2b7a..0000000
Binary files a/portal/assets/fonts/sourcesanspro-regular-webfont.ttf and /dev/null differ
diff --git a/portal/assets/fonts/ynh_ssowat/ynh_ssowat.eot b/portal/assets/fonts/ynh_ssowat/ynh_ssowat.eot
deleted file mode 100644
index 8377071..0000000
Binary files a/portal/assets/fonts/ynh_ssowat/ynh_ssowat.eot and /dev/null differ
diff --git a/portal/assets/fonts/ynh_ssowat/ynh_ssowat.svg b/portal/assets/fonts/ynh_ssowat/ynh_ssowat.svg
deleted file mode 100644
index c1ffa27..0000000
--- a/portal/assets/fonts/ynh_ssowat/ynh_ssowat.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/portal/assets/fonts/ynh_ssowat/ynh_ssowat.ttf b/portal/assets/fonts/ynh_ssowat/ynh_ssowat.ttf
deleted file mode 100644
index 60690e4..0000000
Binary files a/portal/assets/fonts/ynh_ssowat/ynh_ssowat.ttf and /dev/null differ
diff --git a/portal/assets/fonts/ynh_ssowat/ynh_ssowat.woff b/portal/assets/fonts/ynh_ssowat/ynh_ssowat.woff
deleted file mode 100644
index a7333a4..0000000
Binary files a/portal/assets/fonts/ynh_ssowat/ynh_ssowat.woff and /dev/null differ
diff --git a/portal/assets/icons/apple-touch-icon-114x114.png b/portal/assets/icons/apple-touch-icon-114x114.png
deleted file mode 100644
index 45d121c..0000000
Binary files a/portal/assets/icons/apple-touch-icon-114x114.png and /dev/null differ
diff --git a/portal/assets/icons/apple-touch-icon-120x120.png b/portal/assets/icons/apple-touch-icon-120x120.png
deleted file mode 100644
index fa23db1..0000000
Binary files a/portal/assets/icons/apple-touch-icon-120x120.png and /dev/null differ
diff --git a/portal/assets/icons/apple-touch-icon-144x144.png b/portal/assets/icons/apple-touch-icon-144x144.png
deleted file mode 100644
index 16f32b3..0000000
Binary files a/portal/assets/icons/apple-touch-icon-144x144.png and /dev/null differ
diff --git a/portal/assets/icons/apple-touch-icon-152x152.png b/portal/assets/icons/apple-touch-icon-152x152.png
deleted file mode 100644
index f3c0294..0000000
Binary files a/portal/assets/icons/apple-touch-icon-152x152.png and /dev/null differ
diff --git a/portal/assets/icons/apple-touch-icon-57x57.png b/portal/assets/icons/apple-touch-icon-57x57.png
deleted file mode 100644
index ee9f56a..0000000
Binary files a/portal/assets/icons/apple-touch-icon-57x57.png and /dev/null differ
diff --git a/portal/assets/icons/apple-touch-icon-60x60.png b/portal/assets/icons/apple-touch-icon-60x60.png
deleted file mode 100644
index a41fe8c..0000000
Binary files a/portal/assets/icons/apple-touch-icon-60x60.png and /dev/null differ
diff --git a/portal/assets/icons/apple-touch-icon-72x72.png b/portal/assets/icons/apple-touch-icon-72x72.png
deleted file mode 100644
index fd6b339..0000000
Binary files a/portal/assets/icons/apple-touch-icon-72x72.png and /dev/null differ
diff --git a/portal/assets/icons/apple-touch-icon-76x76.png b/portal/assets/icons/apple-touch-icon-76x76.png
deleted file mode 100644
index 4303c46..0000000
Binary files a/portal/assets/icons/apple-touch-icon-76x76.png and /dev/null differ
diff --git a/portal/assets/icons/apple-touch-icon-precomposed.png b/portal/assets/icons/apple-touch-icon-precomposed.png
deleted file mode 100644
index a5ffdad..0000000
Binary files a/portal/assets/icons/apple-touch-icon-precomposed.png and /dev/null differ
diff --git a/portal/assets/icons/apple-touch-icon.png b/portal/assets/icons/apple-touch-icon.png
deleted file mode 100644
index f3c0294..0000000
Binary files a/portal/assets/icons/apple-touch-icon.png and /dev/null differ
diff --git a/portal/assets/icons/browserconfig.xml b/portal/assets/icons/browserconfig.xml
deleted file mode 100644
index fe44cae..0000000
--- a/portal/assets/icons/browserconfig.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
- #da532c
-
-
-
diff --git a/portal/assets/icons/favicon-160x160.png b/portal/assets/icons/favicon-160x160.png
deleted file mode 100644
index dcfd55a..0000000
Binary files a/portal/assets/icons/favicon-160x160.png and /dev/null differ
diff --git a/portal/assets/icons/favicon-16x16.png b/portal/assets/icons/favicon-16x16.png
deleted file mode 100644
index b31f28d..0000000
Binary files a/portal/assets/icons/favicon-16x16.png and /dev/null differ
diff --git a/portal/assets/icons/favicon-196x196.png b/portal/assets/icons/favicon-196x196.png
deleted file mode 100644
index 7f3ff17..0000000
Binary files a/portal/assets/icons/favicon-196x196.png and /dev/null differ
diff --git a/portal/assets/icons/favicon-32x32.png b/portal/assets/icons/favicon-32x32.png
deleted file mode 100644
index 6f0bc53..0000000
Binary files a/portal/assets/icons/favicon-32x32.png and /dev/null differ
diff --git a/portal/assets/icons/favicon-96x96.png b/portal/assets/icons/favicon-96x96.png
deleted file mode 100644
index ba8da95..0000000
Binary files a/portal/assets/icons/favicon-96x96.png and /dev/null differ
diff --git a/portal/assets/icons/favicon.ico b/portal/assets/icons/favicon.ico
deleted file mode 100644
index 5d09b3d..0000000
Binary files a/portal/assets/icons/favicon.ico and /dev/null differ
diff --git a/portal/assets/icons/mstile-144x144.png b/portal/assets/icons/mstile-144x144.png
deleted file mode 100644
index 4dfa436..0000000
Binary files a/portal/assets/icons/mstile-144x144.png and /dev/null differ
diff --git a/portal/assets/icons/mstile-150x150.png b/portal/assets/icons/mstile-150x150.png
deleted file mode 100644
index ac459a2..0000000
Binary files a/portal/assets/icons/mstile-150x150.png and /dev/null differ
diff --git a/portal/assets/icons/mstile-310x150.png b/portal/assets/icons/mstile-310x150.png
deleted file mode 100644
index 564b149..0000000
Binary files a/portal/assets/icons/mstile-310x150.png and /dev/null differ
diff --git a/portal/assets/icons/mstile-310x310.png b/portal/assets/icons/mstile-310x310.png
deleted file mode 100644
index b374084..0000000
Binary files a/portal/assets/icons/mstile-310x310.png and /dev/null differ
diff --git a/portal/assets/icons/mstile-70x70.png b/portal/assets/icons/mstile-70x70.png
deleted file mode 100644
index fbc5b93..0000000
Binary files a/portal/assets/icons/mstile-70x70.png and /dev/null differ
diff --git a/portal/assets/img/logo-ynh-white.svg b/portal/assets/img/logo-ynh-white.svg
deleted file mode 100644
index f960fd3..0000000
--- a/portal/assets/img/logo-ynh-white.svg
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-]>
-
diff --git a/portal/assets/img/logo-ynh.svg b/portal/assets/img/logo-ynh.svg
deleted file mode 100644
index 8f2a56e..0000000
--- a/portal/assets/img/logo-ynh.svg
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-]>
-
diff --git a/portal/assets/js/ynh_portal.js b/portal/assets/js/ynh_portal.js
deleted file mode 100644
index e018167..0000000
--- a/portal/assets/js/ynh_portal.js
+++ /dev/null
@@ -1,401 +0,0 @@
-/*
-===============================================================================
- This JS file is loaded :
- - in the YunoHost user portal
- - on every app page if the app nginx's conf does include the ynh snippet
-===============================================================================
-*/
-
-/*
-=====================
- Utilities
-=====================
-*/
-
-/* Console log fix */
-if (typeof(console) === 'undefined') {
- var console = {};
- console.log = console.error = console.info = console.debug = console.warn = console.trace = console.dir = console.dirxml = console.group = console.groupEnd = console.time = console.timeEnd = console.assert = console.profile = function() {};
-}
-
-/* Cookies utilities */
-function setCookie(cName, cValue, expDays) {
- let date = new Date();
- date.setTime(date.getTime() + (expDays * 24 * 60 * 60 * 1000));
- const expires = "expires=" + date.toUTCString();
- document.cookie = cName + "=" + cValue + "; " + expires + "; path=/";
-}
-function getCookie(cName) {
- const name = cName + "=";
- const cDecoded = decodeURIComponent(document.cookie); //to be careful
- const cArr = cDecoded .split('; ');
- let res;
- cArr.forEach(val => {
- if (val.indexOf(name) === 0) res = val.substring(name.length);
- })
- return res;
-}
-
-/* Array utilities
- https://github.com/Darklg/JavaScriptUtilities/blob/master/assets/js/vanilla-js/libs/vanilla-arrays.js
--------------------------- */
-Array.contains = function(needle, haystack) {
- var i = 0,
- length = haystack.length;
-
- for (; i < length; i++) {
- if (haystack[i] === needle) return true;
- }
- return false;
-};
-Array.each = function(arrayToParse, callback) {
- var i = 0,
- length = arrayToParse.length;
- for (; i < length; i++) {
- callback(arrayToParse[i]);
- }
-};
-
-
-
-/* CSS classes utilities
- https://github.com/Darklg/JavaScriptUtilities/blob/master/assets/js/vanilla-js/libs/vanilla-classes.js
--------------------------- */
-Element.getClassNames = function(element) {
- var classNames = [],
- elementClassName = element.className;
- if (elementClassName !== '') {
- elementClassName = elementClassName.replace(/\s+/g, ' ');
- classNames = elementClassName.split(' ');
- }
- return classNames;
-};
-Element.hasClass = function(element, className) {
- if (element.classList) {
- return element.classList.contains(className);
- }
- return Array.contains(className, Element.getClassNames(element));
-};
-Element.addClass = function(element, className) {
- if (element.classList) {
- element.classList.add(className);
- return;
- }
- if (!Element.hasClass(element, className)) {
- var elementClasses = Element.getClassNames(element);
- elementClasses.push(className);
- element.className = elementClasses.join(' ');
- }
-};
-Element.removeClass = function(element, className) {
- if (element.classList) {
- element.classList.remove(className);
- return;
- }
- var elementClasses = Element.getClassNames(element);
- var newElementClasses = [];
- var i = 0,
- arLength = elementClasses.length;
- for (; i < arLength; i++) {
- if (elementClasses[i] !== className) {
- newElementClasses.push(elementClasses[i]);
- }
- }
- element.className = newElementClasses.join(' ');
-};
-Element.toggleClass = function(element, className) {
- if (!Element.hasClass(element, className)) {
- Element.addClass(element, className);
- }
- else {
- Element.removeClass(element, className);
- }
-};
-
-
-/* Add Event
- https://github.com/Darklg/JavaScriptUtilities/blob/master/assets/js/vanilla-js/libs/vanilla-events.js
--------------------------- */
-window.addEvent = function(el, eventName, callback, options) {
- if (el == null) { return; }
- if (el.addEventListener) {
- if (!options || typeof(options) !== "object") {
- options = {};
- }
-
- options.capture = false;
- el.addEventListener(eventName, callback, options);
- }
- else if (el.attachEvent) {
- el.attachEvent("on" + eventName, function(e) {
- return callback.call(el, e);
- });
- }
-};
-window.eventPreventDefault = function(event) {
- return (event.preventDefault) ? event.preventDefault() : event.returnValue = false;
-};
-
-
-/* Draggable
-
- Sources :
- http://jsfiddle.net/5t3Ju/
- http://stackoverflow.com/questions/9334084/moveable-draggable-div
- http://jsfiddle.net/tovic/Xcb8d/light/
--------------------------- */
-
-function make_element_draggable(id) {
-
- // Variables
- this.elem = document.getElementById(id),
- this.selected = null, // Selected element
- this.dragged = false, // Dragging status
- this.x_pos = 0, this.y_pos = 0, // Stores x & y coordinates of the mouse pointer
- this.x_elem = 0, this.y_elem = 0; // Stores top, left values (edge) of the element
-
- var _initDrag = function(e){
- if (e.type === "touchstart"){
- x_pos = e.touches[0].clientX;
- y_pos = e.touches[0].clientY;
- }
-
- selected = elem;
- x_elem = x_pos - selected.offsetLeft;
- y_elem = y_pos - selected.offsetTop;
-
- // We add listening event for the iframe itself ...
- // otherwise dragging the tile on the iframe doesn't
- // work properly.
- // We do this at click time to have a better chance
- // that the iframe's body is indeed loaded ...
- // (a bit hackish but meh)
- portalOverlay = document.getElementById("ynh-overlay").contentDocument.body;
- window.addEvent(portalOverlay, 'mousemove', _onMove);
- window.addEvent(portalOverlay, 'touchmove', _onMove, {passive: false});
- };
-
- var _shutDrag = function(e){
- selected = null;
- };
-
- var _onMove = function(e){
- // Get position
- x_pos = document.all ? window.event: e.pageX;
- y_pos = document.all ? window.event : e.pageY;
-
- if (e.type === "touchmove") {
- x_pos = e.touches[0].clientX;
- y_pos = e.touches[0].clientY;
- }
-
- if (selected !== null) {
- if (e.type === "touchmove"){
- event.preventDefault();
- }
- dragged = true;
- selected.style.left = (x_pos - x_elem) + 'px';
- selected.style.top = (y_pos - y_elem) + 'px';
- // Store positions in cookies
- setCookie('ynh_overlay_top', selected.style.top, 30);
- setCookie('ynh_overlay_left', selected.style.left, 30);
- }
- };
-
- // Prevent native D'n'D behavior
- window.addEvent(elem, 'dragstart', function(e){
- window.eventPreventDefault(e);
- });
-
- // Start dragging
- window.addEvent(elem, 'mousedown', _initDrag);
- window.addEvent(elem, 'touchstart', _initDrag);
-
- // Will be called when user dragging an element
- window.addEvent(window, 'mousemove', _onMove);
- window.addEvent(window, 'touchmove', _onMove, {passive: false});
-
- // Destroy the object when we are done
- window.addEvent(window, 'mouseup', _shutDrag);
- window.addEvent(window, 'touchend', _shutDrag);
- window.addEvent(window, 'touchcancel', _shutDrag);
-
- // Handle click event
- window.addEvent(elem, 'click', function(e){
- // Prevent default event
- window.eventPreventDefault(e);
-
- // Do not propagate to other click event if dragged out
- if (dragged) {
- e.stopImmediatePropagation();
- }
- // Reset dragging status
- dragged = false;
- });
-};
-
-/* ----------------------------------------------------------
- Main
----------------------------------------------------------- */
-window.addEvent(document, 'DOMContentLoaded', function() {
-
- // 3 different cases :
- // - this script is loaded from inside an app
- // - this script is loaded inside the portal, inside an iframe/overlay activated by clicking the portal button inside an app
- // - this script is loaded inside the "regular" portal when going to /yunohost/sso.
-
- var in_app = ! document.body.classList.contains('ynh-user-portal');
- var in_overlay_iframe = (window.location != window.parent.location);
-
- if (in_app)
- {
- // Do not load inside an app iframe (Roundcube visualisation panel for example).
- if (window.frameElement == null) {
- init_portal_button_and_overlay();
- }
- }
- else
- {
- init_portal();
- if (in_overlay_iframe) { tweak_portal_when_in_iframe(); }
- }
-});
-
-//
-// This function is called when ynh_portal.js is included in an app
-//
-// It will create the small yunohost "portal button" usually in the bottom
-// right corner and initialize the portal overlay, shown when clicking the
-// portal button meant to make it easier to switch between apps.
-//
-function init_portal_button_and_overlay()
-{
- // Set and store meta viewport
- var meta_viewport = document.querySelector('meta[name="viewport"]');
- if (meta_viewport === null) {
- meta_viewport = document.createElement('meta');
- meta_viewport.setAttribute('name', "viewport");
- meta_viewport.setAttribute('content', "");
- document.getElementsByTagName('head')[0].insertBefore(meta_viewport, null);
- }
- meta_viewport = document.querySelector('meta[name="viewport"]');
- meta_viewport_content = meta_viewport.getAttribute('content');
-
- // Prepare and inject the portal overlay (what is activated when clicking on the portal button)
- var portalOverlay = document.createElement('iframe');
- portalOverlay.src = "/yunohost/sso/portal.html";
- portalOverlay.setAttribute("id","ynh-overlay");
- portalOverlay.setAttribute("style","display: none;"); // make sure the overlay is invisible already when loading it
- // portalOverlay.setAttribute("class","ynh-fadeOut"); // set overlay as masked when loading it
- document.body.insertBefore(portalOverlay, null);
-
- // Inject portal button
- var portalButton = document.createElement('a');
- portalButton.setAttribute('id', 'ynh-overlay-switch');
- portalButton.setAttribute('href', '/yunohost/sso/');
- portalButton.setAttribute('class', 'disableAjax');
- // Checks if cookies exist and apply positioning
- if (getCookie('ynh_overlay_top') != null && getCookie('ynh_overlay_left') != null) {
- portalButton.style.top = getCookie('ynh_overlay_top');
- portalButton.style.left = getCookie('ynh_overlay_left');
- }
- document.body.insertBefore(portalButton, null);
- // Make portal button draggable, for user convenience
- make_element_draggable('ynh-overlay-switch');
-
- // Bind portal button
- window.addEvent(portalButton, 'click', function(e){
- // Prevent default click
- window.eventPreventDefault(e);
- // Toggle overlay on YNHPortal button click
- Element.toggleClass(document.querySelector('html'), 'ynh-panel-active');
- Element.toggleClass(portalOverlay, 'ynh-active');
-
- if (Element.hasClass(portalOverlay, 'ynh-active')) {
- portalOverlay.setAttribute("style","display: block;");
- meta_viewport.setAttribute('content', meta_viewport_content);
- Element.addClass(portalOverlay, 'ynh-fadeIn');
- Element.removeClass(portalOverlay, 'ynh-fadeOut');
- } else {
- portalOverlay.setAttribute("style","display: none;");
- meta_viewport.setAttribute('content', "width=device-width");
- Element.removeClass(portalOverlay, 'ynh-fadeIn');
- Element.addClass(portalOverlay, 'ynh-fadeOut');
- }
- });
-}
-
-//
-// This function is called to initialize elements like the app tile colors and other things ...
-//
-function init_portal()
-{
-
- window.addEvent(document.getElementById('add-mailalias'), "click", function() {
- // Clone last input.
- var inputAliasClone = document.querySelector('.mailalias-input').cloneNode(true);
- // Empty value.
- inputAliasClone.value = '';
- // Append to form-group.
- this.parentNode.insertBefore(inputAliasClone, this);
- });
-
- window.addEvent(document.getElementById('add-maildrop'), "click", function() {
- // Clone last input.
- var inputDropClone = document.querySelector('.maildrop-input').cloneNode(true);
- // Empty value.
- inputDropClone.value = '';
- // Append to form-group.
- this.parentNode.insertBefore(inputDropClone, this);
- });
-
- Array.each(document.getElementsByClassName("app-tile"), function(el) {
- // Set first-letter data attribute.
- el.querySelector('.first-letter').innerHTML = el.getAttribute("data-appname").substring(0, 2);
- // handle app links so they work both in plain info page and in the info iframe called from ynh_portal.js
- window.addEvent(el, 'click', function(event) {
- // if asked to open in new tab
- if (event.ctrlKey || event.shiftKey || event.metaKey
- || (event.button && event.button == 1)) {
- return
- }
- // if asked in current tab
- else {
- event.preventDefault();
- parent.location.href=this.href;
- return false;
- };
- });
- });
-}
-
-
-function tweak_portal_when_in_iframe()
-{
- // Set class to body to show we're in overlay
- document.body.classList.add('in_app_overlay');
- let userContainer = document.querySelector('a.user-container');
- if (userContainer) {
- userContainer.classList.replace('user-container-info', 'user-container-edit');
- userContainer.setAttribute('href', userContainer
- .getAttribute('href')
- .replace('edit.html', ''));
- window.addEvent(userContainer, 'click', function(e) {
- e.preventDefault();
- e.stopPropagation();
- window.parent.location.href = userContainer.getAttribute('href');
- });
- }
- let logoutButton = document.getElementById('ynh-logout');
- if (logoutButton)
- {
- // We force to do the logout "globally", not just in the
- // iframe, otherwise after login out the url might still be
- // domain.tld/app which is weird ...
- window.addEvent(logoutButton, 'click', function(e) {
- e.preventDefault();
- e.stopPropagation();
- window.parent.location.href = logoutButton.getAttribute("href");
- });
- }
-}
\ No newline at end of file
diff --git a/portal/assets/themes/clouds/background.jpg b/portal/assets/themes/clouds/background.jpg
deleted file mode 100644
index 32a876c..0000000
Binary files a/portal/assets/themes/clouds/background.jpg and /dev/null differ
diff --git a/portal/assets/themes/clouds/cloud.png b/portal/assets/themes/clouds/cloud.png
deleted file mode 100644
index 1909064..0000000
Binary files a/portal/assets/themes/clouds/cloud.png and /dev/null differ
diff --git a/portal/assets/themes/clouds/custom_overlay.css b/portal/assets/themes/clouds/custom_overlay.css
deleted file mode 100644
index 7f1a000..0000000
--- a/portal/assets/themes/clouds/custom_overlay.css
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
-===============================================================================
- This file may contain extra CSS rules loaded on all apps page (*if* the app
- nginx's conf does include the appropriate snippet) for the small YunoHost
- button in bottom-right corner + portal overlay.
-
- The yunohost button corresponds to : #ynh-overlay-switch
- The yunohost portal overlay / iframe corresponds to : #ynh-overlay
-
- BE CAREFUL that you should *not* add too-general rules that apply to
- non-yunohost elements (for instance all 'a' or 'p' elements...) as it will
- likely break app's rendering
-===============================================================================
-*/
-#ynh-overlay-switch {
- background-image: url("./cloud.png");
-}
diff --git a/portal/assets/themes/clouds/custom_portal.css b/portal/assets/themes/clouds/custom_portal.css
deleted file mode 100644
index 2591ca2..0000000
--- a/portal/assets/themes/clouds/custom_portal.css
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
-===============================================================================
- This file contain extra CSS rules to customize the YunoHost user portal and
- can be used to customize app tiles, buttons, etc...
-===============================================================================
-*/
-
-/* Make page texts black */
-.user-container h2,
-.user-container small,
-.user-container .user-mail,
-.user-container .user-mail,
-.content .footer a,
-a.app-tile,
-#ynh-logout {
- color: black !important;
-}
-
-.ynh-user-portal {
- background-image: url("background.jpg");
- background-repeat: no-repeat;
- background-size: cover;
- width: 100%;
- height: 100%;
-}
-
-/* Apps colors */
-.app-tile {
- background-color: rgba(255, 255, 255, 0.5) !important;
-}
-
-.app-tile:hover:after,
-.app-tile:focus:after,
-.app-tile:hover:before,
-.app-tile:focus:before {
- background: rgba(255, 255, 255, 0.5) !important;
-}
-
-/* Use a custom logo image */
-#ynh-logo {
- z-index: 10;
- background-image: url("./cloud.png");
-}
diff --git a/portal/assets/themes/clouds/custom_portal.js b/portal/assets/themes/clouds/custom_portal.js
deleted file mode 100644
index 80c27bd..0000000
--- a/portal/assets/themes/clouds/custom_portal.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
-===============================================================================
- This JS file may be used to customize the YunoHost user portal *and* also
- will be loaded in all app pages if the app nginx's conf does include the
- appropriate snippet.
-
- You can monkeypatch init_portal (loading of the user portal) and
- init_portal_button_and_overlay (loading of the button and overlay...) to do
- custom stuff
-===============================================================================
-*/
-
-/*
- * Monkeypatch init_portal to customize the app tile style
- *
-init_portal_original = init_portal;
-init_portal = function()
-{
- init_portal_original();
- // Some stuff here
-}
-*/
-
-/*
- * Monkey patching example to do custom stuff when loading inside an app
- *
-init_portal_button_and_overlay_original = init_portal_button_and_overlay;
-init_portal_button_and_overlay = function()
-{
- init_portal_button_and_overlay_original();
- // Custom stuff to do when loading inside an app
-}
-*/
diff --git a/portal/assets/themes/default/custom_overlay.css b/portal/assets/themes/default/custom_overlay.css
deleted file mode 100644
index 0074f3e..0000000
--- a/portal/assets/themes/default/custom_overlay.css
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
-===============================================================================
- This file may contain extra CSS rules loaded on all apps page (*if* the app
- nginx's conf does include the appropriate snippet) for the small YunoHost
- button in bottom-right corner + portal overlay.
-
- The yunohost button corresponds to : #ynh-overlay-switch
- The yunohost portal overlay / iframe corresponds to : #ynh-overlay
-
- BE CAREFUL that you should *not* add too-general rules that apply to
- non-yunohost elements (for instance all 'a' or 'p' elements...) as it will
- likely break app's rendering
-===============================================================================
-*/
diff --git a/portal/assets/themes/default/custom_portal.css b/portal/assets/themes/default/custom_portal.css
deleted file mode 100644
index 7346398..0000000
--- a/portal/assets/themes/default/custom_portal.css
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
-===============================================================================
- This file contain extra CSS rules to customize the YunoHost user portal and
- can be used to customize app tiles, buttons, etc...
-===============================================================================
-*/
-
-.bluebg {
- background: #3498DB!important;
-}
-.bluebg:hover:after,
-.bluebg:focus:after,
-.bluebg:hover:before,
-.bluebg:focus:before {
- background: #16527A!important;
-}
-
-.purplebg {
- background: #9B59B6!important;
-}
-.purplebg:hover:after,
-.purplebg:focus:after,
-.purplebg:hover:before,
-.purplebg:focus:before {
- background: #532C64!important;
-}
-
-.redbg {
- background: #E74C3C!important;
-}
-.redbg:hover:after,
-.redbg:focus:after,
-.redbg:hover:before,
-.redbg:focus:before {
- background: #921E12!important;
-}
-
-.orangebg {
- background: #F39C12!important;
-}
-.orangebg:hover:after,
-.orangebg:focus:after,
-.orangebg:hover:before,
-.orangebg:focus:before {
- background: #7F5006!important;
-}
-
-.greenbg {
- background: #2ECC71!important;
-}
-.greenbg:hover:after,
-.greenbg:focus:after,
-.greenbg:hover:before,
-.greenbg:focus:before {
- background: #176437!important;
-}
-
-.darkbluebg {
- background: #34495E!important;
-}
-.darkbluebg:hover:after,
-.darkbluebg:focus:after,
-.darkbluebg:hover:before,
-.darkbluebg:focus:before {
- background: #07090C!important;
-}
-
-.lightbluebg {
- background: #6A93D4!important;
-}
-.lightbluebg:hover:after,
-.lightbluebg:focus:after,
-.lightbluebg:hover:before,
-.lightbluebg:focus:before {
- background: #2B5394!important;
-}
-
-.yellowbg {
- background: #F1C40F!important;
-}
-.yellowbg:hover:after,
-.yellowbg:focus:after,
-.yellowbg:hover:before,
-.yellowbg:focus:before {
- background: #796307!important;
-}
-
-
-.lightpinkbg {
- background: #F76F87!important;
-}
-.lightpinkbg:hover:after,
-.lightpinkbg:focus:after,
-.lightpinkbg:hover:before,
-.lightpinkbg:focus:before {
- background: #DA0C31!important;
-}
-
-/* Following colors are not used yet */
-.pinkbg {
- background: #D66D92!important;
-}
-.pinkbg:hover:after,
-.pinkbg:focus:after,
-.pinkbg:hover:before,
-.pinkbg:focus:before {
- background: #992B52!important;
-}
-
-.turquoisebg {
- background: #1ABC9C!important;
-}
-.turquoisebg:hover:after,
-.turquoisebg:focus:after,
-.turquoisebg:hover:before,
-.turquoisebg:focus:before {
- background: #0B4C3F!important;
-}
-.lightyellow {
- background: #FFC973!important;
-}
-.lightyellow:hover:after,
-.lightyellow:focus:after,
-.lightyellow:hover:before,
-.lightyellow:focus:before {
- background: #F39500!important;
-}
-.lightgreen {
- background: #B5F36D!important;
-}
-.lightgreen:hover:after,
-.lightgreen:focus:after,
-.lightgreen:hover:before,
-.lightgreen:focus:before {
- background: #77CF11!important;
-}
-.purpledarkbg {
- background: #8E44AD!important;
-}
-.purpledarkbg:hover:after,
-.purpledarkbg:focus:after,
-.purpledarkbg:hover:before,
-.purpledarkbg:focus:before {
- background: #432051!important;
-}
diff --git a/portal/assets/themes/default/custom_portal.js b/portal/assets/themes/default/custom_portal.js
deleted file mode 100644
index 7849e7c..0000000
--- a/portal/assets/themes/default/custom_portal.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-===============================================================================
- This JS file may be used to customize the YunoHost user portal *and* also
- will be loaded in all app pages if the app nginx's conf does include the
- appropriate snippet.
-
- You can monkeypatch init_portal (loading of the user portal) and
- init_portal_button_and_overlay (loading of the button and overlay...) to do
- custom stuff
-===============================================================================
-*/
-
-var app_tile_colors = ['redbg','purpledarkbg','darkbluebg','orangebg','greenbg', 'yellowbg','lightpinkbg','pinkbg','turquoisebg','lightbluebg', 'bluebg'];
-
-function set_app_tile_style(el)
-{
- // Select a color value from the App label
- randomColorNumber = parseInt(el.textContent, 36) % app_tile_colors.length;
- // Add color class.
- el.classList.add(app_tile_colors[randomColorNumber]);
-}
-
-// Monkeypatch init_portal to customize the app tile style
-init_portal_original = init_portal;
-init_portal = function()
-{
- init_portal_original();
- Array.each(document.getElementsByClassName("app-tile"), set_app_tile_style);
-}
-
-/*
- * Monkey patching example to do custom stuff when loading inside an app
- *
-init_portal_button_and_overlay_original = init_portal_button_and_overlay;
-init_portal_button_and_overlay = function()
-{
- init_portal_button_and_overlay_original();
- // Custom stuff to do when loading inside an app
-}
-*/
diff --git a/portal/assets/themes/light/custom_overlay.css b/portal/assets/themes/light/custom_overlay.css
deleted file mode 100644
index a2d0151..0000000
--- a/portal/assets/themes/light/custom_overlay.css
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
-===============================================================================
- This file may contain extra CSS rules loaded on all apps page (*if* the app
- nginx's conf does include the appropriate snippet) for the small YunoHost
- button in bottom-right corner + portal overlay.
-
- The yunohost button corresponds to : #ynh-overlay-switch
- The yunohost portal overlay / iframe corresponds to : #ynh-overlay
-
- BE CAREFUL that you should *not* add too-general rules that apply to
- non-yunohost elements (for instance all 'a' or 'p' elements...) as it will
- likely break app's rendering
-===============================================================================
-*/
-
-#ynh-overlay-switch {
- /* FIXME : idk if this is an issue or not to have /yunohost/sso hard-coded here */
- background-image: url("/yunohost/sso/assets/img/logo-ynh.svg");
- border-color: #eee;
- background-color: #eee;
-}
-
-#ynh-overlay-switch:hover {
- border-color: #ccc;
- background-color: #ccc;
-}
diff --git a/portal/assets/themes/light/custom_portal.css b/portal/assets/themes/light/custom_portal.css
deleted file mode 100644
index 110ac2c..0000000
--- a/portal/assets/themes/light/custom_portal.css
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
-===============================================================================
- This file contain extra CSS rules to customize the YunoHost user portal and
- can be used to customize app tiles, buttons, etc...
-===============================================================================
-*/
-
-body {
- background: #fff;
-}
-
-#ynh-logo {
- background-image: url("../../img/logo-ynh.svg");
-}
-
-.login-form .form-group {
- border: 1px solid #bbb;
-}
-
-.user-container,
-.user-menu a,
-.link-btn,
-.footer a {
- color: #555;
-}
-
-.user-menu a:hover,
-.footer a:hover {
- color: #000;
-}
-
-.form-text:disabled:hover {
- background: #797b83;
-}
-
-.link-btn,
-.link-btn:hover {
- background: none;
-}
-
-
-.bluebg {
- background: #3498DB!important;
-}
-.bluebg:hover:after,
-.bluebg:focus:after,
-.bluebg:hover:before,
-.bluebg:focus:before {
- background: #16527A!important;
-}
-
-.purplebg {
- background: #9B59B6!important;
-}
-.purplebg:hover:after,
-.purplebg:focus:after,
-.purplebg:hover:before,
-.purplebg:focus:before {
- background: #532C64!important;
-}
-
-.redbg {
- background: #E74C3C!important;
-}
-.redbg:hover:after,
-.redbg:focus:after,
-.redbg:hover:before,
-.redbg:focus:before {
- background: #921E12!important;
-}
-
-.orangebg {
- background: #F39C12!important;
-}
-.orangebg:hover:after,
-.orangebg:focus:after,
-.orangebg:hover:before,
-.orangebg:focus:before {
- background: #7F5006!important;
-}
-
-.greenbg {
- background: #2ECC71!important;
-}
-.greenbg:hover:after,
-.greenbg:focus:after,
-.greenbg:hover:before,
-.greenbg:focus:before {
- background: #176437!important;
-}
-
-.darkbluebg {
- background: #34495E!important;
-}
-.darkbluebg:hover:after,
-.darkbluebg:focus:after,
-.darkbluebg:hover:before,
-.darkbluebg:focus:before {
- background: #07090C!important;
-}
-
-.lightbluebg {
- background: #6A93D4!important;
-}
-.lightbluebg:hover:after,
-.lightbluebg:focus:after,
-.lightbluebg:hover:before,
-.lightbluebg:focus:before {
- background: #2B5394!important;
-}
-
-.yellowbg {
- background: #F1C40F!important;
-}
-.yellowbg:hover:after,
-.yellowbg:focus:after,
-.yellowbg:hover:before,
-.yellowbg:focus:before {
- background: #796307!important;
-}
-
-
-.lightpinkbg {
- background: #F76F87!important;
-}
-.lightpinkbg:hover:after,
-.lightpinkbg:focus:after,
-.lightpinkbg:hover:before,
-.lightpinkbg:focus:before {
- background: #DA0C31!important;
-}
-
-/* Following colors are not used yet */
-.pinkbg {
- background: #D66D92!important;
-}
-.pinkbg:hover:after,
-.pinkbg:focus:after,
-.pinkbg:hover:before,
-.pinkbg:focus:before {
- background: #992B52!important;
-}
-
-.turquoisebg {
- background: #1ABC9C!important;
-}
-.turquoisebg:hover:after,
-.turquoisebg:focus:after,
-.turquoisebg:hover:before,
-.turquoisebg:focus:before {
- background: #0B4C3F!important;
-}
-.lightyellow {
- background: #FFC973!important;
-}
-.lightyellow:hover:after,
-.lightyellow:focus:after,
-.lightyellow:hover:before,
-.lightyellow:focus:before {
- background: #F39500!important;
-}
-.lightgreen {
- background: #B5F36D!important;
-}
-.lightgreen:hover:after,
-.lightgreen:focus:after,
-.lightgreen:hover:before,
-.lightgreen:focus:before {
- background: #77CF11!important;
-}
-.purpledarkbg {
- background: #8E44AD!important;
-}
-.purpledarkbg:hover:after,
-.purpledarkbg:focus:after,
-.purpledarkbg:hover:before,
-.purpledarkbg:focus:before {
- background: #432051!important;
-}
diff --git a/portal/assets/themes/light/custom_portal.js b/portal/assets/themes/light/custom_portal.js
deleted file mode 100644
index 7849e7c..0000000
--- a/portal/assets/themes/light/custom_portal.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-===============================================================================
- This JS file may be used to customize the YunoHost user portal *and* also
- will be loaded in all app pages if the app nginx's conf does include the
- appropriate snippet.
-
- You can monkeypatch init_portal (loading of the user portal) and
- init_portal_button_and_overlay (loading of the button and overlay...) to do
- custom stuff
-===============================================================================
-*/
-
-var app_tile_colors = ['redbg','purpledarkbg','darkbluebg','orangebg','greenbg', 'yellowbg','lightpinkbg','pinkbg','turquoisebg','lightbluebg', 'bluebg'];
-
-function set_app_tile_style(el)
-{
- // Select a color value from the App label
- randomColorNumber = parseInt(el.textContent, 36) % app_tile_colors.length;
- // Add color class.
- el.classList.add(app_tile_colors[randomColorNumber]);
-}
-
-// Monkeypatch init_portal to customize the app tile style
-init_portal_original = init_portal;
-init_portal = function()
-{
- init_portal_original();
- Array.each(document.getElementsByClassName("app-tile"), set_app_tile_style);
-}
-
-/*
- * Monkey patching example to do custom stuff when loading inside an app
- *
-init_portal_button_and_overlay_original = init_portal_button_and_overlay;
-init_portal_button_and_overlay = function()
-{
- init_portal_button_and_overlay_original();
- // Custom stuff to do when loading inside an app
-}
-*/
diff --git a/portal/assets/themes/unsplash/cloud.png b/portal/assets/themes/unsplash/cloud.png
deleted file mode 100644
index ad3ea55..0000000
Binary files a/portal/assets/themes/unsplash/cloud.png and /dev/null differ
diff --git a/portal/assets/themes/unsplash/custom_overlay.css b/portal/assets/themes/unsplash/custom_overlay.css
deleted file mode 100644
index 7f1a000..0000000
--- a/portal/assets/themes/unsplash/custom_overlay.css
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
-===============================================================================
- This file may contain extra CSS rules loaded on all apps page (*if* the app
- nginx's conf does include the appropriate snippet) for the small YunoHost
- button in bottom-right corner + portal overlay.
-
- The yunohost button corresponds to : #ynh-overlay-switch
- The yunohost portal overlay / iframe corresponds to : #ynh-overlay
-
- BE CAREFUL that you should *not* add too-general rules that apply to
- non-yunohost elements (for instance all 'a' or 'p' elements...) as it will
- likely break app's rendering
-===============================================================================
-*/
-#ynh-overlay-switch {
- background-image: url("./cloud.png");
-}
diff --git a/portal/assets/themes/unsplash/custom_portal.css b/portal/assets/themes/unsplash/custom_portal.css
deleted file mode 100644
index 00d82a8..0000000
--- a/portal/assets/themes/unsplash/custom_portal.css
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
-===============================================================================
- This file contain extra CSS rules to customize the YunoHost user portal and
- can be used to customize app tiles, buttons, etc...
-===============================================================================
-*/
-
-/* Make page texts white */
-.user-container h2,
-.user-container small,
-.user-container .user-mail,
-.user-container .user-mail,
-.content .footer a,
-a.app-tile,
-#ynh-logout {
- color: white !important;
-}
-
-body {
- color: white !important;
- text-shadow: 3px 4px 4px rgba(0,0,0,.4), -1px -1px 6px rgba(0,0,0,0.2);
-}
-
-.ynh-user-portal {
- background-image: url('https://source.unsplash.com/random/featured/?nature') !important;
- background-repeat: no-repeat;
- background-size: cover;
- width: 100%;
- height: 100%;
-}
-
-/* Apps colors */
-.app-tile {
- background-color: rgba(255, 255, 255, 0.5) !important;
-}
-
-.app-tile:hover:after,
-.app-tile:focus:after,
-.app-tile:hover:before,
-.app-tile:focus:before {
- background: rgba(255, 255, 255, 0.5) !important;
-}
-
-/* Use a custom logo image */
-#ynh-logo {
- z-index: 10;
- background-image: url("./cloud.png");
-}
-
-/* Round the form */
-.login-form label:before {
- border-top-left-radius: 5em ;
- border-bottom-left-radius: 5em ;
-}
-
-.login-form * {
- border-radius: 5em;
-}
-
-/* Make form black */
-
-.login-form label::before {
- background: #000;
- color: #FFF;
-}
-
-.login-form .form-group * {
- background: #000;
- color: #FFF;
-}
-
-.icon {
- background: #000;
-}
-
-.messages {
- border-radius: .5em;
-}
diff --git a/portal/assets/themes/vapor/custom_overlay.css b/portal/assets/themes/vapor/custom_overlay.css
deleted file mode 100644
index 0074f3e..0000000
--- a/portal/assets/themes/vapor/custom_overlay.css
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
-===============================================================================
- This file may contain extra CSS rules loaded on all apps page (*if* the app
- nginx's conf does include the appropriate snippet) for the small YunoHost
- button in bottom-right corner + portal overlay.
-
- The yunohost button corresponds to : #ynh-overlay-switch
- The yunohost portal overlay / iframe corresponds to : #ynh-overlay
-
- BE CAREFUL that you should *not* add too-general rules that apply to
- non-yunohost elements (for instance all 'a' or 'p' elements...) as it will
- likely break app's rendering
-===============================================================================
-*/
diff --git a/portal/assets/themes/vapor/custom_portal.css b/portal/assets/themes/vapor/custom_portal.css
deleted file mode 100644
index ead4d5b..0000000
--- a/portal/assets/themes/vapor/custom_portal.css
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
-===============================================================================
- This file contain extra CSS rules to customize the YunoHost user portal and
- can be used to customize app tiles, buttons, etc...
-===============================================================================
-*/
-
-/* ==========================================================================
- Vaporwave theme
- ========================================================================== */
-.ynh-user-portal {
- min-height: 100vh;
- background: rgb(205, 118, 255) !important;
- background: -moz-linear-gradient(45deg, rgb(205, 118, 255) 0%, rgb(93, 150, 168) 100%) !important;
- background: -webkit-gradient(linear, left bottom, right top, color-stop(0%, rgb(205, 118, 255)), color-stop(100%, rgb(93, 150, 168))) !important;
- background: -webkit-linear-gradient(45deg, rgb(205, 118, 255) 0%, rgb(93, 150, 168) 100%) !important;
- background: -o-linear-gradient(45deg, rgb(205, 118, 255) 0%, rgb(93, 150, 168) 100%) !important;
- background: -ms-linear-gradient(45deg, rgb(205, 118, 255) 0%, rgb(93, 150, 168) 100%) !important;
- background: linear-gradient(45deg, rgb(205, 118, 255) 0%, rgb(93, 150, 168) 100%) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#C82BFF', endColorstr='#0C76A8', GradientType=1) !important;
-}
-
-.messages.danger { background: #c0392b80; }
-.messages.warning { background: #e67e2280; }
-.messages.success { background: #27ae6080; }
-.messages.info { background: #2980b980; }
-
-a, small, span,
-.ynh-wrapper.footer a,
-.user-menu a,
-.user-container.user-container-info span,
-input.btn.classic-btn.large-btn {
- color: #e0e0e0 !important;
-}
-
-.form-group input::placeholder,
-.form-group input::-ms-input-placeholder,
-.form-group input:-ms-input-placeholder {
- color: #f4f4f4 !important;
-}
-
-form.login-form input {
- color: #222 !important;
-}
-
-a:hover,
-a:active,
-a:focus,
-.form-group input,
-input.btn.classic-btn.large-btn:hover,
-.ynh-wrapper.footer a:hover {
- color: white !important;
-}
-
-.ynh-wrapper.footer a:before {
- color: #cc45ee !important;
-}
-
-.ynh-wrapper.footer nav {
- border-color: #cc45ee !important;
-}
-
-.listing-apps li a span,
-.listing-apps li a:hover span,
-.listing-apps li a:active span,
-.listing-apps li a:focus span {
- color: white !important;
-}
-
-.listing-apps li,
-.listing-apps li a {
- transition: all 0.3s ease-in-out, background 0ms; /* fix gray flicker on initial load */
- border: none transparent !important;
- box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.1),
- -2px -2px 3px 0 rgba(0, 0, 0, 0.1) inset;
-}
-
-.listing-apps li:hover,
-.listing-apps li a:hover {
- box-shadow: 2px 2px 3px rgba(0, 0, 0, 0),
- -2px -2px 3px 0 rgba(0, 0, 0, 0) inset;
-}
-
-.btn.large-btn.classic-btn,
-.btn.large-btn.validate-btn {
- background: rgba(200, 200, 200, 0.4) !important;
-}
-
-.btn.large-btn.classic-btn:hover,
-.btn.large-btn.validate-btn:hover {
- background: rgba(255, 255, 255, 0.4) !important;
-}
-
-/* There are no colors, there is only vapor! */
-.app-tile,
-.form-group input,
-.form-group label,
-a.btn:hover,
-.btn.large-btn {
- background: rgba(200, 200, 200, 0.2) !important;
- border: none;
-}
-
-.app-tile:hover:after,
-.app-tile:focus:after,
-.app-tile:hover:before,
-.app-tile:focus:before {
- background: rgba(200, 200, 200, 0.4) !important;
-}
diff --git a/portal/assets/themes/vapor/custom_portal.js b/portal/assets/themes/vapor/custom_portal.js
deleted file mode 100644
index 80c27bd..0000000
--- a/portal/assets/themes/vapor/custom_portal.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
-===============================================================================
- This JS file may be used to customize the YunoHost user portal *and* also
- will be loaded in all app pages if the app nginx's conf does include the
- appropriate snippet.
-
- You can monkeypatch init_portal (loading of the user portal) and
- init_portal_button_and_overlay (loading of the button and overlay...) to do
- custom stuff
-===============================================================================
-*/
-
-/*
- * Monkeypatch init_portal to customize the app tile style
- *
-init_portal_original = init_portal;
-init_portal = function()
-{
- init_portal_original();
- // Some stuff here
-}
-*/
-
-/*
- * Monkey patching example to do custom stuff when loading inside an app
- *
-init_portal_button_and_overlay_original = init_portal_button_and_overlay;
-init_portal_button_and_overlay = function()
-{
- init_portal_button_and_overlay_original();
- // Custom stuff to do when loading inside an app
-}
-*/
diff --git a/portal/edit.html b/portal/edit.html
deleted file mode 100644
index d3f58c0..0000000
--- a/portal/edit.html
+++ /dev/null
@@ -1,59 +0,0 @@
-