3 [ -n "$include_users" ] && return 0
6 . "${_EXEC}/cgilite/session.sh"
7 . "${_EXEC}/cgilite/storage.sh"
9 SENDMAIL=${SENDMAIL-sendmail}
11 USER_REGISTRATION="${USER_REGISTRATION-true}"
12 USER_REQUIREEMAIL="${USER_REQUIREEMAIL-true}"
13 USER_ACCOUNTPAGE="${USER_ACCOUNTPAGE}"
15 USER_ACCOUNTEXPIRE="${USER_ACCOUNTEXPIRE:-$((86400 * 730))}"
16 USER_CONFIRMEXPIRE="${USER_CONFIRMEXPIRE:-86400}"
18 MAILFROM="${MAILDOMAIN-noreply@${HTTP_HOST%:*}}"
20 HTTP_HOST="$(HEADER Host)"
22 [ "$HTTPS" ] && SCHEMA=https || SCHEMA=http
25 # UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
26 # (pending|active|deleted)
30 USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
31 USER_EXPIRE USER_DEVICES USER_FUTUREUSE
35 USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
36 USER_EXPIRE USER_DEVICES USER_FUTUREUSE
40 # override all functions marked with "TRANSLATION"
41 # sed -n '/TRANSLATION$/,/^}/p;' <cgilite/users.sh
46 user_db="${user_db:-${_DATA}/users.db}"
52 USER_ID='' USER_NAME='' USER_STATUS='' USER_EMAIL='' USER_PWSALT=''
53 USER_PWHASH='' USER_EXPIRE='' USER_DEVICES='' USER_FUTUREUSE=''
56 read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
57 USER_EXPIRE USER_DEVICES USER_FUTUREUSE
58 elif [ "$user" -a -f "$user_db" -a -r "$user_db" ]; then
59 read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
60 USER_EXPIRE USER_DEVICES USER_FUTUREUSE <<-EOF
61 $(grep "^${user} " "${user_db}")
64 if [ "$USER_ID" -a "${USER_EXPIRE:-0}" -gt "$_DATE" ]; then
65 USER_NAME="$(UNSTRING "$USER_NAME")"
66 USER_EMAIL="$(UNSTRING "$USER_EMAIL")"
67 USER_DEVICES="$(UNSTRING "$USER_DEVICES")"
68 unset USER_PWSALT USER_PWHASH
76 # internal function for user update
77 local uid="$1" uname status email pwsalt pwhash expire devices futureuse
78 local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
81 for arg in "$@"; do case $arg in
82 uname=*) uname="${arg#*=}";;
83 status=*) status="${arg#*=}";;
84 email=*) email="${arg#*=}";;
85 password=*) pwsalt="$(randomid)"; pwhash="$(user_pwhash "$pwsalt" "${arg#*=}")";;
86 expire=*) expire="${arg#*=}";;
87 devices=*) devices="${arg#*=}";;
90 if LOCK "$user_db"; then
91 while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES \
93 if [ "$UID_" = "$uid" ]; then
94 printf '%s %s %s %s %s %s %i %s %s\n' \
95 "$uid" "$(STRING "${uname-$(UNSTRING "$UNAME")}")" \
96 "${status:-${status-${STATUS}}${status+\\}}" \
97 "${email:-${email-${EMAIL}}${email+\\}}" \
98 "${pwsalt:-${PWSALT}}" "${pwhash:-${PWHASH}}" \
99 "${expire:-$((_DATE + USER_ACCOUNTEXPIRE))}" \
100 "$(STRING "${devices-$(UNSTRING "$DEVICES")}")" \
102 elif [ "$STATUS" = pending -a ! "$EXPIRE" -ge "$_DATE" ]; then
103 # omit expired invitations from output
106 printf '%s %s %s %s %s %s %i %s %s\n' \
107 "$UID_" "$UNAME" "$STATUS" "$EMAIL" "$PWSALT" "$PWHASH" \
108 "$EXPIRE" "$DEVICES" "$FUTUREUSE"
110 done <"$user_db" >"${user_db}.$$"
111 mv -- "${user_db}.$$" "$user_db"
119 local user="${1:-$(timeid)}"
122 if LOCK "$user_db"; then
123 if grep -q "^${user} " "$user_db"; then
127 printf '%s \\ %s \\ \\ \\ %i \\ \\\n' \
128 "$user" "pending" "$(( _DATE + USER_CONFIRMEXPIRE ))" >>"$user_db"
133 if [ $# -eq 0 ]; then
136 elif update_user "$user" "$@"; then
148 if [ ! "$USER_IDMAP" ]; then
150 USER_IDMAP="${USER_IDMAP}${USER_ID} ${USER_NAME}${BR}"
153 if [ "$uid" -a "$USER_IDMAP" != "${USER_IDMAP##*${uid} }" ]; then
154 ret="${USER_IDMAP##*${uid} }"; ret="${ret%%${BR}*}";
157 elif [ "$uid" ]; then
160 printf '%s' "$USER_IDMAP"
166 local name="$(STRING "$1")" ret
167 [ "$USER_IDMAP" ] || user_idmap >/dev/null
169 if [ "${name%\\}" -a "$USER_IDMAP" != "${USER_IDMAP% ${name}${BR}*}" ]; then
170 ret="${USER_IDMAP% ${name}${BR}*}"; ret="${ret##*${BR}}"
179 { [ $# -gt 0 ] && printf %s "$*" || cat; } \
185 /^[a-zA-Z][a-zA-Z0-9 -~]{2,127}$/!d;
191 { [ $# -gt 0 ] && printf %s "$*" || cat; } \
193 # W3C recommended email regex
194 # https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email)
195 /^[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;
200 local uname="$(STRING "$1")"
201 local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
202 [ -f "$user_db" -a -r "$user_db" ] \
203 && while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE; do
204 [ "$EXPIRE" -gt "$_DATE" -a "$UNAME" = "$uname" ] && return 0
210 local email="$(STRING "$1")"
211 local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
212 [ -f "$user_db" -a -r "$user_db" ] \
213 && while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE; do
214 [ "$EXPIRE" -gt "$_DATE" -a "$EMAIL" = "$email" ] && return 0
220 local salt="$1" secret="$2" hash
221 hash="$(printf '%s\n%s\n' "$secret" "$salt" |sha256sum)"
222 printf '%s\n' "${hash%% *}"
225 user_register_email() { # TRANSLATION
226 "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
229 Subject: Your account registration at ${HTTP_HOST%:*}
231 Someone tried to sign up for a user account using this email address.
233 You can activate your account using this link:
235 ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
237 This registration link will expire after $((USER_CONFIRMEXPIRE / 3600)) hours.
239 If you did not request an account at ${HTTP_HOST%:*}, then someone else
240 probably entered your email address by accident. In this case you shoud
241 simply ignore this message and we will remove your email address from
242 our database within the next day.
244 This is an automatic email. Any direct reply will not be received.
245 Your Account Registration Robot.
250 # reserve account, send registration mail
251 # preliminary uid, expiration, signature
252 local uid="$(timeid)"
253 local uname="$(POST uname |user_checkname)"
254 local email="$(POST email |user_checkemail)"
255 local pwsalt="$(randomid)"
256 local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
258 if [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
259 REDIRECT "${_BASE}${PATH_INFO}#ERROR_REGISTRATION_DISABLED"
262 if [ "$USER_REQUIREEMAIL" = true ]; then
263 if [ ! "email" ]; then
264 REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
265 elif user_emailexist "$email"; then
266 REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
267 elif new_user "$uid" status=pending email="$email" expire="$((_DATE + USER_CONFIRMEXPIRE))"; then
268 debug "Sending Activation Link:" \
269 "${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
271 REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
273 REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
276 elif [ "$USER_REQUIREEMAIL" != true ]; then
277 if [ ! "$uname" ]; then
278 REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_INVALID"
279 elif user_nameexist "$uname"; then
280 REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_EXISTS"
281 elif [ ! "$pw" ]; then
282 REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_EMPTYTOOSHORT"
283 elif [ "$pw" != "$pwconfirm" ]; then
284 REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH"
285 elif new_user "$uid" uname="$uname" status=active email="$email" password="$pw" expire="$((_DATE + USER_ACCOUNTEXPIRE))"; then
287 SESSION_BIND user_id "$uid"
289 if [ "$USER_ACCOUNTPAGE" ]; then
290 REDIRECT "${USER_ACCOUNTPAGE}"
292 REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
295 REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
300 user_invite_email(){ # TRANSLATION
301 "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
304 Subject: You have been invited to ${HTTP_HOST%:*}
306 ${USER_NAME:-Someone} has offered an invitation to this email address.
310 You can create your account using this link:
312 ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
314 This registration link will expire after $((USER_CONFIRMEXPIRE / 3600)) hours.
316 If you do not know what this is about, then someone else probably
317 entered your email address by accident. In this case you shoud
318 simply ignore this message and we will remove your email address from
319 our database within the next day.
321 This is an automatic email. Any direct reply will not be received.
322 Your Account Registration Robot.
327 local uid="$(timeid)"
328 local email="$(POST email |user_checkemail)"
329 local message="$(POST message)"
331 if [ ! "email" ]; then
332 REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
333 elif user_emailexist "$email"; then
334 REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
335 elif new_user "$uid" status=pending email="$email" expire="$((_DATE + USER_CONFIRMEXPIRE))"; then
336 debug "Sending Invitation Link:" \
337 "${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
339 REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
341 REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
348 local uid="$(POST uid |checkid || printf invalid)"
349 local signature="$(POST signature)"
350 local uname="$(POST uname |user_checkname)"
351 local pwsalt="$(randomid)"
352 local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
356 if [ "$signature" != "$(session_mac "$uid")" ]; then
357 REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
358 elif [ ! "$uname" ]; then
359 REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_INVALID"
360 elif user_nameexist "$uname"; then
361 REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_EXISTS"
362 elif [ ! "$pw" ]; then
363 REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_EMPTYTOOSHORT"
364 elif [ "$pw" != "$pwconfirm" ]; then
365 REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_MISMATCH"
366 elif [ "$USER_STATUS" != pending -o \! "$USER_EXPIRE" -gt "$_DATE" ]; then
367 REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
368 elif update_user "$USER_ID" uname="$uname" status=active password="$pw"; then
370 SESSION_BIND user_id "$USER_ID"
371 if [ "$USER_ACCOUNTPAGE" ]; then
372 REDIRECT "${USER_ACCOUNTPAGE}"
374 REDIRECT "${_BASE}${PATH_INFO}?user_register=confirm#USER_REGISTER_CONFIRM"
377 REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
383 # keep logged in - device cookie?
384 # initialize new session!
385 local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
386 local uname="$(POST uname |STRING)" pw="$(POST pw)"
388 [ -f "$user_db" -a -r "$user_db" ] \
389 && while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE; do
390 if [ "$UNAME" = "$uname" -o "$EMAIL" = "$uname" ]; then
391 if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$pw")" ]; then
393 SESSION_BIND user_id "$UID_"
394 REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN"
398 REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_LOGIN"
402 # destroy cookie, destroy session
406 SET_COOKIE 0 user_id="" Path="/${_BASE#/}" SameSite=Strict HttpOnly
407 REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_OUT"
411 # todo: username update, email update / email confirm
412 local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
413 # local uname="$(POST uname |STRING)"
414 local uid oldpw pw pwconfirm
417 oldpw="$(POST oldpw)"
418 pw="$(POST pw |grep -xE '.{6}')"
419 pwconfirm="$(POST pwconfirm)"
422 read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE <<-EOF
423 $(grep "^${uid} " "$user_db")
426 if [ "$UID_" = "$USER_ID" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$oldpw")" ]; then
427 if [ "$pw" -a "$pw" = "$pwconfirm" ]; then
428 update_user "${uid}" password="$pw"
429 REDIRECT "${_BASE}${PATH_INFO}#UPDATE_SUCCESS"
431 REDIRECT "${_BASE}${PATH_INFO}#ERROR_PWMISMATCH"
433 elif [ "$UID_" = "$USER_ID" ]; then
434 REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_AUTH_PASSWORD"
436 REDIRECT "${_BASE}${PATH_INFO}#ERROR_NOTLOGGEDIN"
448 read_user "$(SESSION_VAR user_id)"
449 [ "$USER_STATUS" -a "$USER_STATUS" != active ] && eval $UNSET_USER
451 [ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in
452 user_register) user_register ;;
453 user_confirm) user_confirm ;;
454 user_invite) user_invite ;;
455 user_login) user_login ;;
456 user_logout) user_logout ;;
457 user_update) user_update ;;
464 export USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
465 USER_EXPIRE USER_DEVICES USER_FUTUREUSE
469 if [ ! "$USER_ID" ]; then
471 [div #user_update .nouser
472 This page can only be used by registered users
477 [form #user_update method=POST
478 [hidden "uid" "$USER_ID"]
479 [p .username Logged in as $USER_NAME]
480 [input type=password name=oldpw placeholder="Current Passphrase"]
481 [input type=password name=pw placeholder="New Passphrase" pattern=".{6,}"]
482 [input type=password name=pwconfirm placeholder="Confirm New Passphrase" pattern=".{6,}"]
483 [submit "action" "user_update" Update Passphrase]
489 w_user_register_disabled(){ # TRANSLATION
491 [div #user_register .disabled
492 User Registration is disabled.
496 w_user_register_sendmail(){ # TRANSLATION
498 [form #user_register .registeremail method=POST
499 [p We will send an activation mail to your email address.
500 You can continue the signup process when you click on the
501 activation link in this email.]
502 [input type=email name=email placeholder="Email"]
503 [submit "action" "user_register" Sign Up]
507 w_user_register_direct(){ # TRANSLATION
509 [form #user_register .registername method=POST
510 [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="^\[\\\\p{L}\]\[\\\\p{L}0-9 -~\]{2,127}$" autocomplete=off]
511 [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
512 [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
513 [submit "action" "user_register" Sign Up]
519 if [ "$(GET user_confirm)" ]; then
521 elif [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
522 w_user_register_disabled
523 elif [ "$USER_REQUIREEMAIL" = true ]; then
524 w_user_register_sendmail
525 elif [ "$USER_REQUIREEMAIL" != true ]; then
526 w_user_register_direct
530 w_user_confirm_proceed(){ # TRANSLATION
532 [form #user_confirm method=POST
533 [input type=hidden name=uid value="${uid}"]
534 [input type=hidden name=signature value="${signature}"]
535 $([ "$EMAIL" != '\' ] && printf \
536 '[input disabled=disabled value="%s" placeholder="Email"]' "$(UNSTRING "$EMAIL" |HTML)"
538 [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="^\[\\\\p{L}\]\[\\\\p{L}0-9 -~\]{2,127}$" autocomplete=off]
539 [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
540 [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
541 [submit "action" "user_confirm" Finish Registration]
545 w_user_confirm_expired(){ # TRANSLATION
547 [div #user_confirm .expired
548 [p This activation link is not valid anymore.]
552 w_user_confirm_invalid(){ # TRANSLATION
554 [div #user_confirm .invalid
555 [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.]
561 local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
562 local user_confirm="$(GET user_confirm)"
563 local uid="${user_confirm% *}" signature="${user_confirm#* }"
565 if [ "$signature" = "$(session_mac "$uid")" ]; then
566 read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE <<-EOF
567 $(grep "^${uid} " "$user_db")
569 if [ "$STATUS" = pending -a "$EXPIRE" -gt "$_DATE" ]; then
570 w_user_confirm_proceed
572 w_user_confirm_expired
575 w_user_confirm_invalid
579 w_user_invite_email(){ # TRANSLATION
581 [form #user_invite method=POST
582 [input placeholder="Email Recipient" name=email autocomplete=off]
583 [textarea name="message" placeholder="Message to recipient" . ]
584 [submit "action" "user_invite" Send Invitation]
588 w_user_invite_link(){ # TRANSLATION
590 [div #user_invite .link
591 [p An anonymous user account has been set up. Send the following link to the intended user, so they may claim their account. The link will remain valid for $((USER_CONFIRMEXPIRE / 3600)) hours.]
592 [a href="$(HTML "$invlink")" . $(HTML "$invlink")]
594 [p [a href="#" . Set up another account]]
598 w_user_invite_deny(){ # TRANSLATION
600 [div #user_invite .notallowed
601 Only registered users may send an invitation to another user.
609 if [ "$(GET user_confirm)" ]; then
611 elif [ "$USER_ID" -a "$SENDMAIL" ]; then
613 elif [ "$USER_ID" ]; then
615 new_user "$uid" status=pending expire="$((_DATE + USER_CONFIRMEXPIRE))"
616 invlink="${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
617 debug "New Invitation Link: $invlink"
624 w_user_login_logon(){ # TRANSLATION
626 [form #user_login .login method=POST
627 [input name=uname placeholder="Username or Email" autocomplete=off]
628 [input type=password name=pw placeholder="Passphrase"]
629 [submit "action" "user_login" Login]
633 w_user_login_logoff(){ # TRANSLATION
635 [form #user_login .logout method=POST
636 [p Logged in as [span . $(HTML ${USER_NAME})]]
637 [submit "action" "user_logout" Logout]
643 if [ ! "$USER_ID" ]; then
645 elif [ "$USER_ID" ]; then