]> git.plutz.net Git - cgilite/blob - users.sh
user invite function, handle invite/registration expire, always allow registration...
[cgilite] / users.sh
1 #!/bin/sh
2
3 [ -n "$include_users" ] && return 0
4 include_users="$0"
5
6 . "${_EXEC}/cgilite/session.sh"
7 . "${_EXEC}/cgilite/storage.sh"
8
9 USER_REGISTRATION="${USER_REGISTRATION:-true}"
10 USER_REQUIREEMAIL="${USER_REQUIREEMAIL:-true}"
11
12 HTTP_HOST="$(HEADER Host)"
13 MAILFROM="${MAILDOMAIN:-noreply@${HTTP_HOST%:*}}"
14
15 user_db="${_DATA}/users.db"
16 unset USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
17       USER_EXPIRE USER_DEVICES USER_FUTUREUSE
18
19 # USER DB
20 # UID   UNAME   STATUS (pending|active|deleted) EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
21
22 user_id="$(SESSION_VAR user_id)"
23 if [ "$user_id" -a -f "$user_db" -a -r "$user_db" ]; then
24    read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
25            USER_EXPIRE USER_DEVICES USER_FUTUREUSE <<-EOF
26         $(grep "^${user_id}     " "$user_db")
27         EOF
28   if [ "$USER_ID" -a "$USER_STATUS" = active -a "$USER_EXPIRE" -gt "$_DATE" ]; then
29        USER_NAME="$(UNSTRING "$USER_NAME")"
30       USER_EMAIL="$(UNSTRING "$USER_EMAIL")"
31     USER_DEVICES="$(UNSTRING "$USER_DEVICES")"
32     unset USER_PWSALT USER_PWHASH
33   else
34     unset USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
35           USER_EXPIRE USER_DEVICES USER_FUTUREUSE
36   fi
37 fi
38 unset user_id
39
40 user_update_user() {
41   # internal function for user update
42   local uid="$1" uname status email pwsalt pwhash expire devices futureuse
43   local UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
44   local arg
45
46   for arg in "$@"; do case $arg in
47     uname=*) uname="${arg#*=}";;
48     status=*) status="${arg#*=}";;
49     email=*) email="${arg#*=}";;
50     password=*) pwsalt="$(randomid)"; pwhash="$(user_pwhash "$pwsalt" "${arg#*=}")";;
51     devices=*) devices="${arg#*=}";;
52   esac; done
53
54   if LOCK "$user_db"; then
55     while read -r UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES \
56                   FUTUREUSE; do
57     if [ "$UID" = "$uid" ]; then
58       printf '%s        %s      %s      %s      %s      %s      %s      %s      %s\n' \
59              "$uid" "$(STRING "${uname-$(UNSTRING "$UNAME")}")" \
60              "${status:-${status-${STATUS}}${status+\\}}" \
61              "${email:-${email-${EMAIL}}${email+\\}}" \
62              "${pwsalt:-${PWSALT}}" "${pwhash:-${PWHASH}}" \
63              "$((_DATE + 86400 * 730))" \
64              "$(STRING "${devices-$(UNSTRING "$DEVICES")}")" \
65              "${FUTUREUSE:-\\}"
66     elif [ "$STATUS" = pending -a ! "$EXPIRE" -ge "$_DATE" ]; then
67       # omit expired invitations from output
68       :
69     else
70       printf '%s        %s      %s      %s      %s      %s      %s      %s      %s\n' \
71              "$UID" "$UNAME" "$STATUS" "$EMAIL" "$PWSALT" "$PWHASH" \
72              "$EXPIRE" "$DEVICES" "$FUTUREUSE"
73     fi
74     done <"$user_db" >"${user_db}.$$"
75     mv -- "${user_db}.$$" "$user_db"
76     RELEASE "$user_db"
77   else
78     return 1
79   fi
80 }
81
82 user_checkname(){
83   { [ $# -gt 0 ] && printf %s "$*" || cat; } \
84   | sed -nE '
85     :X; $!{N;bX;}
86     s;[ \t\r\n]+; ;g;
87     s;^ ;;; s; $;;;
88     /@/d;
89     /^[a-zA-Z][a-zA-Z0-9 -~]{2,127}$/!d;
90     p;
91     '
92 }
93
94 user_checkemail(){
95   { [ $# -gt 0 ] && printf %s "$*" || cat; } \
96   | sed -nE '
97     # W3C recommended email regex
98     # https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email)
99     /^[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;
100     '
101 }
102
103 user_nameexist(){
104   local uname="$(STRING "$1")"
105   local UID     UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
106   [ -f "$user_db" -a -r "$user_db" ] \
107   && while read -r UID  UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
108     [ "$EXPIRE" -gt "$_DATE" -a "$UNAME" = "$uname" ] && return 0
109   done <"$user_db"
110   return 1
111 }
112
113 user_emailexist(){
114   local email="$(STRING "$1")"
115   local UID     UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
116   [ -f "$user_db" -a -r "$user_db" ] \
117   && while read -r UID  UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
118     [ "$EXPIRE" -gt "$_DATE" -a "$EMAIL" = "$email" ] && return 0
119   done <"$user_db"
120   return 1
121 }
122
123 user_pwhash(){
124   local salt="$1" secret="$2" hash
125   hash="$(printf '%s\n%s\n' "$secret" "$salt" |sha256sum)"
126   printf '%s\n' "${hash%% *}"
127 }
128
129 user_register(){
130   # reserve account, send registration mail
131   # preliminary uid, expiration, signature
132   local uid="$(timeid)"
133   local uname="$(POST uname |user_checkname)"
134   local email="$(POST email |user_checkemail)"
135   local pwsalt="$(randomid)"
136   local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
137
138   if [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
139     REDIRECT "${_BASE}${PATH_INFO}#ERROR_REGISTRATION_DISABLED"
140   fi
141
142   if   [ "$USER_REQUIREEMAIL" = true ]; then
143     if [ ! "email" ]; then
144       REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
145     elif user_emailexist "$email"; then
146       REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
147     elif LOCK "$user_db"; then
148       printf '%s        \\      pending %s      \\      \\      %i      \\      \\\n' \
149              "$uid" "$(STRING "$email")" "$(( $_DATE + 86400 ))" \
150              >>"$user_db"
151       RELEASE "$user_db"
152
153       debug "Sending Activation Link:" \
154             "https://${HTTP_HOST%:*}/${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
155       sendmail -t -f "$MAILFROM" <<-EOF
156         From: ${MAILFROM}
157         To: ${email}
158         Subject: Your account registration at ${HTTP_HOST%:*}
159
160         Someone tried to sign up for a user account using this email address.
161
162         You can activate your account using this link:
163
164             https://${HTTP_HOST%:*}/${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
165
166         This registration link will expire after 24 hours.
167
168         If you did not request an account at ${HTTP_HOST%:*}, then someone else
169         probably entered your email address by accident. In this case you shoud
170         simply ignore this message and we will remove your email address from
171         our database within the next day.
172
173         This is an automatic email. Any direct reply will not be received.
174         Your Account Registration Robot.
175         EOF
176       REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
177     else
178       REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
179     fi
180
181   elif [ "$USER_REQUIREEMAIL" != true ]; then
182     if [ ! "$uname" ]; then
183       REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_INVALID"
184     elif user_nameexist "$uname"; then
185       REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_EXISTS"
186     elif [ ! "$pw" ]; then
187       REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_EMPTYTOOSHORT"
188     elif [ "$pw" != "$pwconfirm" ]; then
189       REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH"
190     elif LOCK "$user_db"; then
191       printf '%s        %s      active  %s      %s      %s      %i      \\      \\\n' \
192              "$uid" "$(STRING "$uname")" "$(STRING "$email")" \
193              "$pwsalt" "$(user_pwhash "$pwsalt" "$pw")" \
194              "$(( $_DATE + 86400 * 730 ))" \
195              >>"$user_db"
196       RELEASE "$user_db"
197
198       SESSION_COOKIE new
199       SESSION_BIND user_id "$uid"
200
201       REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
202     else
203       REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
204     fi
205   fi
206 }
207
208 user_invite(){
209   local uid="$(timeid)"
210   local email="$(POST email |user_checkemail)"
211   local message="$(POST message)"
212
213   if [ ! "email" ]; then
214     REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
215   elif user_emailexist "$email"; then
216     REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
217   elif LOCK "$user_db"; then
218     printf '%s  \\      pending %s      \\      \\      %i      \\      \\\n' \
219            "$uid" "$(STRING "$email")" "$(( $_DATE + 86400 ))" \
220            >>"$user_db"
221     RELEASE "$user_db"
222     debug "Sending Invitation Link:" \
223           "https://${HTTP_HOST%:*}/${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
224     sendmail -t -f "$MAILFROM" <<-EOF
225         From: ${MAILFROM}
226         To: ${email}
227         Subject: You have been invited to ${HTTP_HOST%:*}
228
229         ${USER_NAME:-Someone} has offered an invitation to this email address.
230
231         ${message}
232
233         You can create your account using this link:
234
235             https://${HTTP_HOST%:*}/${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
236
237         This registration link will expire after 24 hours.
238
239         If you do not know what this is about, then someone else probably
240         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.
243
244         This is an automatic email. Any direct reply will not be received.
245         Your Account Registration Robot.
246         EOF
247     REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
248   else
249     REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
250   fi
251 }
252
253 user_confirm(){
254   # enable account
255   local UID     UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
256   local uid="$(POST uid |checkid)"
257   local signature="$(POST signature)"
258   local uname="$(POST uname |user_checkname)"
259   local pwsalt="$(randomid)"
260   local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
261
262   read -r UID   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
263         $(grep "^${uid:-invalid}        " "$user_db")
264         EOF
265
266   if [ "$signature" != "$(session_mac "$uid")" ]; then
267     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
268   elif [ ! "$uname" ]; then
269     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_INVALID"
270   elif user_nameexist "$uname"; then
271     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_EXISTS"
272   elif [ ! "$pw" ]; then
273     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_EMPTYTOOSHORT"
274   elif [ "$pw" != "$pwconfirm" ]; then
275     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_MISMATCH"
276   elif [ "$STATUS" != pending -o \! "$EXPIRE" -gt "$_DATE" ]; then
277     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
278   elif user_update_user "$UID" uname="$uname" status=active password="$pw"; then
279     SESSION_COOKIE new
280     SESSION_BIND user_id "$UID"
281     REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
282   else
283     REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
284   fi
285 }
286
287 user_login(){
288   # set cookie
289   # keep logged in - device cookie?
290   # initialize new session!
291   local UID     UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
292   local uname="$(POST uname |STRING)" pw="$(POST pw)"
293
294   [ -f "$user_db" -a -r "$user_db" ] \
295   && while read -r UID  UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
296     if [ "$UNAME" = "$uname" -o "$EMAIL" = "$uname" ]; then
297       if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$pw")" ]; then
298         SESSION_COOKIE new
299         SESSION_BIND user_id "$UID"
300         REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN"
301       fi
302     fi
303   done <"$user_db"
304   REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_LOGIN"
305 }
306
307 user_logout(){
308   # destroy cookie, destroy session
309   # keep device cookie
310   new_session
311   SESSION_COOKIE new
312   SET_COOKIE 0 user_id="" Path="/${_BASE#/}" SameSite=Strict HttpOnly
313   REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_OUT"
314 }
315
316 user_update(){
317   # passphrase, email
318   :
319 }
320 user_recover(){
321   # send recover link
322   :
323 }
324 user_disable(){
325   :
326 }
327
328 [ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in
329   user_register) user_register ;;
330   user_confirm)  user_confirm ;;
331   user_invite)   user_invite ;;
332   user_login)    user_login ;;
333   user_logout)   user_logout ;;
334   user_update)
335     :;;
336   user_recover)
337     :;;
338   user_disable)
339     :;;
340 esac
341
342 w_user_register(){
343   if [ "$(GET user_confirm)" ]; then
344     w_user_confirm
345   elif [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
346     cat <<-EOF
347         [div #user_register .disabled
348         User Registration is disabled.
349         ]
350         EOF
351   elif [ "$USER_REQUIREEMAIL" = true ]; then
352     cat <<-EOF
353         [form #user_register .registeremail method=POST
354           [p We will send an activation mail to your email address.
355             You can continue the signup process when you click on the
356             activation link in this email.]
357           [input type=email name=email placeholder="Email"]
358           [submit "action" "user_register" Sign Up]
359         ]
360         EOF
361   elif [ "$USER_REQUIREEMAIL" != true ]; then
362     cat <<-EOF
363         [form #user_register .registername method=POST
364           [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]
365           [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
366           [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
367           [submit "action" "user_register" Sign Up]
368         ]
369         EOF
370   fi
371 }
372
373 w_user_confirm(){
374   local UID     UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
375   local user_confirm="$(GET user_confirm)"
376   local uid="${user_confirm% *}" signature="${user_confirm#* }"
377
378   if [ "$signature" = "$(session_mac "$uid")" ]; then
379     read -r UID UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
380         $(grep "^${uid} " "$user_db")
381         EOF
382     if [ "$STATUS" = pending -a "$EXPIRE" -gt "$_DATE" ]; then
383       cat <<-EOF
384         [form #user_confirm method=POST
385           [input type=hidden name=uid value="${uid}"]
386           [input type=hidden name=signature value="${signature}"]
387           [input disabled=disabled value="$(HTML "$EMAIL")"]
388           [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]
389           [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
390           [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
391           [submit "action" "user_confirm" Finish Registration]
392         ]
393         EOF
394     else
395       cat <<-EOF
396         [div #user_confirm .expired
397           [p This activation link is not valid anymore.]
398         ]
399         EOF
400     fi
401   else
402     cat <<-EOF
403         [div #user_confirm .invalid
404           [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.]
405         ]
406         EOF
407   fi
408 }
409
410 w_user_invite(){
411   if [ "$(GET user_confirm)" ]; then
412     w_user_confirm
413   elif [ "$USER_ID" ]; then
414     cat <<-EOF
415         [form #user_invite method=POST
416           [input placeholder="Email Recipient" name=email autocomplete=off]
417           [textarea name="message" placeholder="Message to recipient" . ]
418           [submit "action" "user_invite" Send Invitation]
419         ]
420         EOF
421   else
422     cat <<-EOF
423         [div #user_invite .notallowed
424           Only registered users may send an invitation to another user.
425         ]
426         EOF
427   fi
428 }
429
430 w_user_login(){
431   if [ ! "$USER_ID" ]; then
432     cat <<-EOF
433         [form #user_login .login method=POST
434           [input name=uname placeholder="Username or Email" autocomplete=off]
435           [input type=password name=pw placeholder="Passphrase"]
436           [submit "action" "user_login" Login]
437         ]
438         EOF
439   elif [ "$USER_ID" ]; then
440     cat <<-EOF
441         [form #user_login .logout method=POST
442           [p Logged in as [span . $(HTML ${USER_NAME})]]
443           [submit "action" "user_logout" Logout]
444         ]
445         EOF
446   fi
447 }