From: Paul Hänsch Date: Thu, 16 Sep 2021 00:01:40 +0000 (+0200) Subject: user account functions X-Git-Url: https://git.plutz.net/?a=commitdiff_plain;h=142f5b0b60216dddb94ef30684c300b633f269ed;p=cgilite user account functions --- diff --git a/users.sh b/users.sh new file mode 100755 index 0000000..0af2320 --- /dev/null +++ b/users.sh @@ -0,0 +1,337 @@ +#!/bin/sh + +[ -n "$include_users" ] && return 0 +include_users="$0" + +. "${_EXEC}/cgilite/session.sh" +. "${_EXEC}/cgilite/storage.sh" + +USER_REGISTRATION="${USER_REGISTRATION:-true}" +USER_REQUIREEMAIL="${USER_REQUIREEMAIL:-true}" + +HTTP_HOST="$(HEADER Host)" +MAILFROM="${MAILDOMAIN:-noreply@${HTTP_HOST%:*}}" + +user_db="${_DATA}/users.db" +unset USER_ID USER_NAME USER_EMAIL + +# USER DB +# UID UNAME STATUS (pending|active|deleted) EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + +user_init(){ + local user_id="$(SESSION_VAR user_id)" + local UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + [ "$user_id" ] \ + && read -r UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE <<-EOF + $(grep "^${user_id} " "$user_db") + EOF + if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" ]; then + USER_ID="$UID" + USER_NAME="$(UNSTRING "$UNAME")" + USER_EMAIL="$(UNSTRING "$EMAIL")" + fi +} + +user_checkname(){ + { [ $# -gt 0 ] && printf %s "$*" || cat } \ + | sed -nE ' + :X; $!{N;bX;} + s;[ \t\r\n]+; ;g; + s;^ ;;; s; $;;; + /@/d; + /^[a-zA-Z][a-zA-Z0-9 -~]{2,127}$/!d; + p; + ' +} + +user_checkemail(){ + { [ $# -gt 0 ] && printf %s "$*" || cat } \ + | sed -nE ' + # W3C recommended email regex + # https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email) + /^[a-zA-Z0-9.!#$%&'\''*+\/=?^_`{|}~-]+@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/p; + ' +} + +user_nameexist(){ + local uname="$(STRING "$1")" + local UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + while read -r UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE; do + [ "$EXPIRE" -gt "$_DATE" -a "$UNAME" = "$uname" ] && return 0 + done <"$user_db" + return 1 +} + +user_emailexist(){ + local email="$(STRING "$1")" + local UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + while read -r UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE; do + [ "$EXPIRE" -gt "$_DATE" -a "$EMAIL" = "$email" ] && return 0 + done <"$user_db" + return 1 +} + +user_pwhash(){ + local salt="$1" secret="$2" hash + hash="$(printf '%s\n%s\n' "$secret" "$salt" |sha256sum)" + printf '%s\n' "${hash% *}" +} + +user_register(){ + # reserve account, send registration mail + # preliminary uid, expiration, signature + local uid="$(timeid)" + local uname="$(POST uname |user_checkname)" + local email="$(POST email |user_checkemail)" + local pwsalt="$(randomid)" + local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)" + + if [ "$USER_REGISTRATION" != true ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_REGISTRATION_DISABLED" + fi + + if [ "$USER_REQUIREEMAIL" = true ]; then + if [ ! "email" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID" + elif user_emailexist "$email"; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS" + elif LOCK "$user_db"; then + printf '%s \\ pending %s \\ \\ %i \\ \\\n' \ + "$uid" "$(STRING "$email")" "$(( $_DATE + 86400 ))" \ + >>"$user_db" + RELEASE "$user_db" + sendmail -t -f "$MAILFROM" <<-EOF + From: ${MAILFROM} + To: "${email}" + Subject: Your account registration at ${HTTP_HOST%:*} + + Someone tried to sign up for a user account using this email address. + + You can activate your account using this link: + + https://${HTTP_HOST%:*}/${_BASE}/?user_confirm=${uid}+$(session_mac "$uid") + + This registration link will expire after 24 hours. + + If you did not request an account at ${HTTP_HOST%:*}, then someone else + probably entered your email address by accident. In this case you shoud + simply ignore this message and we will remove your email address from + our database within the next day. + + This is an automatic email. Any direct reply will not be received. + Your Account Registration Robot. + EOF + REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM" + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK" + fi + + elif [ "$USER_REQUIREEMAIL" != true ] then + if [ ! "$uname" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_INVALID" + elif user_nameexist "$uname"; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_EXISTS" + elif [ ! "$pw" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_EMPTYTOOSHORT" + elif [ "$pw" != "$pwconfirm" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH" + elif LOCK "$user_db"; then + printf '%s %s active %s %s %s %i \\ \\\n' \ + "$uid" "$(STRING "$uname")" "$(STRING "$email")" \ + "$pwsalt" "$(user_pwhash "$pwsalt" "$pw")" \ + "$(( $_DATE + 86400 * 730 ))" \ + >>"$user_db" + RELEASE "$user_db" + + SESSION_COOKIE new + SESSION_BIND user_id "$uid" + + REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM" + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK" + fi + fi +} + +user_confirm(){ + # enable account + local UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + local uid="$(POST uid |checkid)" + local signature="$(POST signature)" + local uname="$(POST uname |user_checkname)" + local pwsalt="$(randomid)" + local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)" + + if [ "$signature" != "$(session_mac "$uid")" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID" + elif [ ! "$uname" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_INVALID" + elif user_nameexist "$uname"; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_EXISTS" + elif [ ! "$pw" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_EMPTYTOOSHORT" + elif [ "$pw" != "$pwconfirm" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_MISMATCH" + elif LOCK "$user_db"; then + read -r UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE <<-EOF + $(grep "^${uid} " "$user_db") + EOF + + if [ "$STATUS" != pending -o "$EXPIRE" -le "$_DATE" ]; then + RELEASE "$user_db" + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID" + else + printf '%s %s active %s %s %s %i %s %s\n' \ + "$UID" "$(STRING "$uname")" "$EMAIL" \ + "$pwsalt" "$(user_pwhash "$pwsalt" "$pw")" \ + "$(( $_DATE + 86400 * 730 ))" "$DEVICES" "$FUTUREUSE" \ + >"${user_db}.$$" + grep -v "^${uid} " "$user_db" >>"${user_db}.$$" + mv "${user_db}.$$" "${user_db}" + RELEASE "$user_db" + + SESSION_COOKIE new + SESSION_BIND user_id "$UID" + REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM" + fi + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK" + fi +} + +user_login(){ + # set cookie + # keep logged in - device cookie? + # initialize new session! + local UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + local uname="$(POST uname |STRING)" pw="$(POST pw)" + + while read -r UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE; do + if [ "$UNAME" = "$uname" -o "$EMAIL" = "$uname" ]; then + if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$pw")" ]; then + SESSION_COOKIE new + SESSION_BIND user_id "$UID" + REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN" + fi + fi + done <"$user_db" + REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_LOGIN" +} + +user_logout(){ + # destroy cookie, destroy session + # keep device cookie + new_session + SET_COOKIE 0 session="" + SET_COOKIE 0 user_id="" + REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_OUT" +} + +user_update(){ + # passphrase, email +} +user_recover(){ + # send recover link +} +user_disable(){ +} + +user_init + +[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in + user_register) user_register ;; + user_confirm) user_confirm ;; + user_login) user_login ;; + user_logout) user_logout ;; + user_update) + :;; + user_recover) + :;; + user_disable) + :;; +esac + +w_user_register(){ + if [ "$USER_REGISTRATION" != true ]; then + cat <<-EOF + [div #user_register .disabled + User Registration is disabled. + ] + EOF + elif [ "$USER_REQUIREEMAIL" = true ]; then + cat <<-EOF + [form #user_register .registeremail method=POST + [p We will send an activation mail to your email address. + You can continue the signup process when you click on the + activation link in this email.] + [input type=email name=email placeholder="Email"] + [submit "action" "user_register" Sign Up] + ] + EOF + elif [ "$USER_REQUIREEMAIL" != true ]; then + cat <<-EOF + [form #user_register .registername method=POST + [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[a-zA-Z\]\[a-zA-Z0-9 -~\]{2,127}$" autocomplete=off] + [input type=pw placeholder="Choose Passphrase" pattern=".{4,}"] + [input type=pwconfirm placeholder="Confirm Passphrase" pattern=".{4,}"] + [submit "action" "user_register" Sign Up] + ] + EOF + fi +} + +w_user_confirm(){ + local UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + local user_confirm="$(GET user_confirm)" + local uid="${user_confirm% *}" signature="${user_confirm#* }" + + if [ "$signature" = "$(session_mac "$uid")" ]; then + read -r UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE <<-EOF + $(grep "^${uid} " "$user_db") + EOF + if [ "$STATUS" = pending -a "$EXPIRE" -gt "$_DATE" ]; then + cat <<-EOF + [form #user_confirm method=POST + [input type=hidden name=uid value="${uid}"] + [input type=hidden name=signature value="${signature}"] + [input disabled=disabled value="$(HTML "$EMAIL")"] + [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[a-zA-Z\]\[a-zA-Z0-9 -~\]{2,127}$" autocomplete=off] + [input type=pw placeholder="Choose Passphrase" pattern=".{4,}"] + [input type=pwconfirm placeholder="Confirm Passphrase" pattern=".{4,}"] + [submit "action" "user_confirm" Finish Registration] + ] + EOF + else + cat <<-EOF + [div #user_confirm .expired + [p This activation link is not valid anymore.] + ] + EOF + fi + else + cat <<-EOF + [div #user_confirm .invalid + [p This activation link is invalid. Make sure you copied the whole activation link from your email and be careful not to include any line breaks.] + ] + EOF + fi +} + +w_user_login(){ + if [ ! "$USER_ID" ]; then + cat <<-EOF + [form #user_login .login method=POST + [input name=uname placeholder="Username or Email" autocomplete=off] + [input type=password name=pw placeholder="Passphrase"] + [submit "action" "user_login" Login] + ] + EOF + elif [ "$USER_ID" ]; then + cat <<-EOF + [form #user_login .logout method=POST + [p You are currently logged in as "${USER_NAME}"] + [submit "action" "user_logout" Logout] + ] + EOF + fi +}