New member applications
authoradamch <adamch>
Mon, 2 May 2005 03:45:38 +0000 (03:45 +0000)
committeradamch <adamch>
Mon, 2 May 2005 03:45:38 +0000 (03:45 +0000)
15 files changed:
.cvsignore
app.sig [new file with mode: 0644]
app.sml [new file with mode: 0644]
app/.cvsignore [new file with mode: 0644]
app/after.mlt [new file with mode: 0644]
app/app.sig [new file with mode: 0644]
app/app.sml [new file with mode: 0644]
app/before.mlt [new file with mode: 0644]
app/confirm.mlt [new file with mode: 0644]
app/exn.mlt [new file with mode: 0644]
app/footer.mlt [new file with mode: 0644]
app/header.mlt [new file with mode: 0644]
app/join.mlt [new file with mode: 0644]
app/mlt.conf [new file with mode: 0644]
tables.sql

index e25901e..a2a33e4 100644 (file)
@@ -1,3 +1,2 @@
-out
 .cm
 CM
diff --git a/app.sig b/app.sig
new file mode 100644 (file)
index 0000000..0711e74
--- /dev/null
+++ b/app.sig
@@ -0,0 +1,15 @@
+signature APP =
+sig
+    datatype status =
+            CONFIRMING
+          | PENDING
+          | ACCEPTED
+          | REJECTED
+
+    type app = { id : int, name : string, rname : string, email : string,
+                forward : bool, uses : string, other : string, passwd : string,
+                status : status, stamp : Init.C.timestamp }
+
+    val lookupApp : int -> app
+
+end
\ No newline at end of file
diff --git a/app.sml b/app.sml
new file mode 100644 (file)
index 0000000..5e2c547
--- /dev/null
+++ b/app.sml
@@ -0,0 +1,46 @@
+structure App :> APP =
+struct
+
+open Init Sql Util
+
+datatype status =
+        CONFIRMING
+       | PENDING
+       | ACCEPTED
+       | REJECTED
+
+val statusFromInt =
+    fn 0 => CONFIRMING
+     | 1 => PENDING
+     | 2 => ACCEPTED
+     | 3 => REJECTED
+     | _ => raise C.Sql "Bad status"
+
+val statusToInt =
+    fn CONFIRMING => 0
+     | PENDING => 1
+     | ACCEPTED => 2
+     | REJECTED => 3
+
+fun statusFromSql v = statusFromInt (C.intFromSql v)
+fun statusToSql s = C.intToSql (statusToInt s)
+
+type app = { id : int, name : string, rname : string, email : string,
+            forward : bool, uses : string, other : string,
+            passwd : string, status : status, stamp : C.timestamp }
+
+fun mkAppRow [id, name, rname, email, forward, uses, other, passwd, status, stamp] =
+    { id = C.intFromSql id, name = C.stringFromSql name, rname = C.stringFromSql rname,
+      email = C.stringFromSql email, forward = C.boolFromSql forward,
+      uses = C.stringFromSql uses, other = C.stringFromSql other, passwd = C.stringFromSql passwd,
+      status = statusFromSql status, stamp = C.timestampFromSql stamp }
+  | mkAppRow r = rowError ("app", r)
+
+fun lookupApp id =
+    case C.oneOrNoRows (getDb ()) ($`SELECT id, name, rname, email, forward, uses, other, passwd, status, stamp
+                                       FROM MemberApp
+                                       WHERE id = ^(C.intToSql id)`) of
+       SOME row => mkAppRow row
+      | NONE => raise Fail "Membership application not found"
+
+end
\ No newline at end of file
diff --git a/app/.cvsignore b/app/.cvsignore
new file mode 100644 (file)
index 0000000..854c999
--- /dev/null
@@ -0,0 +1,2 @@
+.cm
+CM
\ No newline at end of file
diff --git a/app/after.mlt b/app/after.mlt
new file mode 100644 (file)
index 0000000..26bef72
--- /dev/null
@@ -0,0 +1,3 @@
+<% App.done () %>
+
+<!-- After -->
\ No newline at end of file
diff --git a/app/app.sig b/app/app.sig
new file mode 100644 (file)
index 0000000..188aaa4
--- /dev/null
@@ -0,0 +1,18 @@
+signature APP =
+sig
+    structure C : SQL_CLIENT
+
+    val init : unit -> unit
+    val done : unit -> unit
+
+    type application = { name : string, rname : string, email : string,
+                        forward : bool, uses : string, other : string }
+
+    val apply : application -> bool
+
+    val validEmail : string -> bool
+    val validUser : string -> bool
+    val userExists : string -> bool
+
+    val confirm : int * string -> bool
+end
\ No newline at end of file
diff --git a/app/app.sml b/app/app.sml
new file mode 100644 (file)
index 0000000..0f4f83a
--- /dev/null
@@ -0,0 +1,143 @@
+structure App :> APP =
+struct
+
+val baseUrl = "http://join.hcoop.net/join/"
+val portalUrl = "http://users.hcoop.net/portal/"
+
+open Sql
+
+structure C = PgClient
+
+val db = ref (NONE : C.conn option)
+
+val rnd = ref (Random.rand (0, 0))
+
+fun init () = 
+    let
+       val c = C.conn "dbname='hcoop'"
+    in
+       db := SOME c;
+       C.dml c "BEGIN";
+       rnd := Random.rand (SysWord.toInt (Posix.Process.pidToWord (Posix.ProcEnv.getpid ())),
+                           SysWord.toInt (Posix.Process.pidToWord (Posix.ProcEnv.getpid ())))
+    end
+
+fun getDb () = valOf (!db)
+
+fun done () =
+    let
+       val c = getDb ()
+    in
+       C.dml c "COMMIT";
+       C.close c;
+       db := NONE
+    end
+
+fun sendMail (to, subj, intro, footer, id) =
+    let
+       val (name, rname, forward, uses, other) =
+           case C.oneOrNoRows (getDb ()) ($`SELECT name, rname, forward, uses, other FROM MemberApp WHERE id = ^(C.intToSql id)`) of
+               SOME [name, rname, forward, uses, other] => (C.stringFromSql name, C.stringFromSql rname,
+                                                            C.boolFromSql forward, C.stringFromSql uses,
+                                                            C.stringFromSql other)
+             | _ => raise Fail "Bad sendMail row"
+
+       val proc = Unix.execute ("/usr/sbin/exim4", ["-t"])
+       fun mwrite s = TextIO.output (Unix.textOutstreamOf proc, s)
+    in
+       mwrite ("From: Hcoop Application Confirmation <join@hcoop.net>\nTo: ");
+       mwrite (to);
+       mwrite ("\nSubject: ");
+       mwrite subj;
+       mwrite ("\n\n");
+       mwrite intro;
+       mwrite ("\n\nUsername: ");
+       mwrite (name);
+       mwrite ("\nReal name: ");
+       mwrite (rname);
+       mwrite ("\nForward e-mail: ");
+       mwrite (if forward then "yes" else "no");
+       mwrite ("\n\nDesired uses:\n");
+       mwrite (uses);
+       mwrite ("\n\nOther information:\n");
+       mwrite (other);
+       mwrite ("\n\n");
+       footer mwrite;
+       OS.Process.isSuccess (Unix.reap proc)
+    end
+
+type application = { name : string, rname : string, email : string,
+                    forward : bool, uses : string, other : string }
+
+fun apply {name, rname, email, forward, uses, other} =
+    let
+       val db = getDb ()
+    in
+       case C.oneRow db ($`SELECT nextval('MemberAppSeq')`) of
+           [id] =>
+           let
+               val id = C.intFromSql id
+               val passwd = Int.toString (Int.abs (Random.randInt (!rnd)))
+           in
+               C.dml db ($`INSERT INTO MemberApp (id, name, rname, email, forward, uses, other, passwd, status, applied)
+                           VALUES (^(C.intToSql id), ^(C.stringToSql name), ^(C.stringToSql rname),
+                                   ^(C.stringToSql email), ^(C.boolToSql forward), ^(C.stringToSql uses),
+                                   ^(C.stringToSql other), ^(C.stringToSql passwd), 0, CURRENT_TIMESTAMP)`);
+               sendMail (email, "Confirm membership application",
+                         "We've received a request to join the Internet Hosting Cooperative (hcoop.net) with this e-mail address.",
+                         fn mwrite => (mwrite ("To confirm this application, visit ");
+                                       mwrite (baseUrl);
+                                       mwrite ("confirm?id=");
+                                       mwrite (Int.toString id);
+                                       mwrite ("&p=");
+                                       mwrite (passwd);
+                                       mwrite ("\n")),
+                         id)
+           end
+      | _ => raise Fail "Bad next sequence val"
+    end
+
+fun isIdent ch = Char.isLower ch orelse Char.isDigit ch
+
+fun validHost s =
+    size s > 0 andalso size s < 20 andalso List.all isIdent (String.explode s)
+                                                 
+fun validDomain s =
+    size s > 0 andalso size s < 100 andalso List.all validHost (String.fields (fn ch => ch = #".") s)
+
+fun validUser s =
+    size s > 0 andalso size s < 50 andalso List.all
+                                              (fn ch => isIdent ch orelse ch = #"." orelse ch = #"_" orelse ch = #"-" orelse ch = #"+")
+                                              (String.explode s)
+
+fun validEmailUser s =
+    size s > 0 andalso size s < 50 andalso List.all
+                                              (fn ch => Char.isAlphaNum ch orelse ch = #"." orelse ch = #"_" orelse ch = #"-" orelse ch = #"+")
+                                              (String.explode s)
+
+fun validEmail s =
+    (case String.fields (fn ch => ch = #"@") s of
+        [user, host] => validEmailUser user andalso validDomain host
+       | _ => false)
+
+fun userExists name =
+    (Posix.SysDB.getpwnam name; true) handle OS.SysErr _ => false
+
+fun confirm (id, passwd) =
+    let
+       val db = getDb ()
+    in
+       case C.oneOrNoRows db ($`SELECT id FROM MemberApp WHERE id = ^(C.intToSql id) AND passwd = ^(C.stringToSql passwd) AND status = 0`) of
+           SOME _ =>
+           (C.dml db ($`UPDATE MemberApp SET status = 1, confirmed = CURRENT_TIMESTAMP WHERE id = ^(C.intToSql id)`);
+            sendMail ("board.fake@hcoop.net",
+                      "New membership application",
+                      "We've received a new request to join hcoop.",
+                      fn mwrite => (mwrite ("Open applications: ");
+                                    mwrite (portalUrl);
+                                    mwrite ("apps")),
+                      id))
+         | NONE => false
+    end
+
+end
\ No newline at end of file
diff --git a/app/before.mlt b/app/before.mlt
new file mode 100644 (file)
index 0000000..9217d71
--- /dev/null
@@ -0,0 +1,3 @@
+<% App.init () %>
+
+<!-- Before -->
\ No newline at end of file
diff --git a/app/confirm.mlt b/app/confirm.mlt
new file mode 100644 (file)
index 0000000..74f583f
--- /dev/null
@@ -0,0 +1,14 @@
+<% @header [("title", ["Confirm appliaction"])];
+
+val id = Web.stoi ($"id");
+val passwd = $"p";
+
+if App.confirm (id, passwd) then
+       %><h3><b>Confirmation successful</b></h3>
+       You should hear from us within a few days from now.<%
+else
+       %><h3><b>Error confirming</b></h3>
+       Did you already follow this confirmation link?<%
+end;
+
+@footer[] %>
\ No newline at end of file
diff --git a/app/exn.mlt b/app/exn.mlt
new file mode 100644 (file)
index 0000000..43fd157
--- /dev/null
@@ -0,0 +1,21 @@
+<html><head>
+<title>Hcoop: Exception</title>
+</head><body>
+
+<h1><b>Exception</b></h1>
+
+<% switch Web.getExn () of
+     Fail msg => %>
+<b>Fail</b>: <% Web.htmlNl msg %>
+<% | App.C.Sql msg => %>
+<b>SQL</b>: <% Web.htmlNl msg %>
+<% | Web.Format s => %>
+<b>Format</b>: <% Web.htmlNl s %>
+<% | ex => %>
+<b>Unknown exception kind.</b> Backtrace:
+<% foreach s in SMLofNJ.exnHistory ex do %>
+<li> <% Web.html s %></li>
+<% end
+end %>
+
+</body></html>
diff --git a/app/footer.mlt b/app/footer.mlt
new file mode 100644 (file)
index 0000000..8634a86
--- /dev/null
@@ -0,0 +1 @@
+</body></html>
\ No newline at end of file
diff --git a/app/header.mlt b/app/header.mlt
new file mode 100644 (file)
index 0000000..de7c81a
--- /dev/null
@@ -0,0 +1,9 @@
+<html><head>
+<% val title =
+       case $"title" of
+                 "" => "Hcoop"
+               | t => ("Hcoop: " ^ t) %>
+<title><% Web.html title %></title>
+</head><body>
+
+<h2><b><% Web.html title %></b></h2>
diff --git a/app/join.mlt b/app/join.mlt
new file mode 100644 (file)
index 0000000..bfbfe28
--- /dev/null
@@ -0,0 +1,52 @@
+<% @header [("title", ["Apply for membership"])] %>
+
+<% if $"cmd" = "app" then
+       val name = $"name";
+       val rname = $"rname";
+       val email = $"email";
+       val forward = $"forward" <> "on";
+       val uses = $"uses";
+       val other = $"other";
+
+       if name = "" then
+               %><h3><b>Please enter a username</b></h3><%
+       elseif rname = "" then
+               %><h3><b>Please enter your name</b></h3><%
+       elseif email = "" then
+               %><h3><b>Please enter your contact e-mail address</b></h3><%
+       elseif uses = "" then
+               %><h3><b>Please enter your proposed uses</b></h3><%
+       elseif not (App.validUser name) then
+               %><h3><b>Invalid requested username</b></h3><%
+       elseif App.userExists name then
+               %><h3><b>That username is already in use.</b><h3><%
+       elseif not (App.validEmail email) then
+               %><h3><b>Invalid e-mail address</b></h3><%
+       elseif not (App.apply { name = name, rname = rname, email = email,
+                               forward = forward, uses = uses, other = other }) then
+               %><h3><b>Error sending confirmation e-mail</b></h3><%
+       else
+               %><h3><b>Application recorded</b></h3>
+               Check your e-mail for a message with further instructions.<%
+       end
+else %>
+
+<form action="join" method="post">
+<input type="hidden" name="cmd" value="app">
+<table>
+<tr> <td align="right" valign="top"><b>Desired username</b>:</td> <td><input name="name"><br>
+       You should follow usual UNIX conventions, and it's helpful to pick a name you wouldn't mind using to identify yourself to strangers.</td> </tr>
+<tr> <td align="right"><b>Your "real" name</b>:</td> <td><input name="rname"></td> </tr>
+<tr> <td align="right"><b>Contact e-mail address</b></td> <td><input name="email"></td> </tr>
+<tr> <td align="right" valign="top"><input type="checkbox" name="forward"></td> <td>Check this box if you would like to use hcoop as your primary e-mail provider.<br>
+       If you don't select this option and you are approved to join, e-mail to your account will be forwarded to the address you provide here.<br>
+       You can change this option later, but we'll probably have helpul things to e-mail you as soon as you join.  It's important that we be able to reach members reliably, so please don't decide to use us as your primary e-mail provider unless you can commit to checking your hcoop mailbox just as often as any other personal accounts you have.</td> </tr>
+<tr> <td align="right" valign="top"><b>How do you plan to use a hcoop membership?</b></td> <td><textarea name="uses" rows="5" cols="80" wrap="soft"></textarea></td> </tr>
+<tr> <td align="right" valign="top"><b>Any other information about yourself</b></td> <td><textarea name="other" rows="5" cols="80" wrap="soft"></textarea></td> </tr>
+<tr> <td><input type="submit" value="Apply"></td> </tr>
+</table>
+</form>
+
+<% end %>
+
+<% @footer[] %>
\ No newline at end of file
diff --git a/app/mlt.conf b/app/mlt.conf
new file mode 100644 (file)
index 0000000..a3134a7
--- /dev/null
@@ -0,0 +1,12 @@
+print real = Util.printReal
+print int  = Util.printInt
+
+before before
+after  after
+exn    exn
+
+out    out
+pub    /var/www/join.hcoop.net/cgi/join
+
+cm     /usr/local/share/smlsql/smlsql.cm
+cm     /usr/local/share/smlsql/libpq/sources.cm
index f1c980d..264ecb8 100644 (file)
@@ -210,3 +210,27 @@ CREATE SEQUENCE MailingListSeq START 1;
 CREATE TABLE DirectoryPref(
        usr INTEGER PRIMARY KEY,
        FOREIGN KEY (usr) REFERENCES WebUser(id) ON DELETE CASCADE);
+
+CREATE TABLE MemberApp(
+       id INTEGER PRIMARY KEY,
+       name TEXT NOT NULL,
+       rname TEXT NOT NULL,
+       email TEXT NOT NULL,
+       forward BOOLEAN NOT NULL,
+       uses TEXT NOT NULL,
+       other TEXT NOT NULL,
+       passwd TEXT NOT NULL,
+       status INTEGER NOT NULL,
+       applied TIMESTAMP NOT NULL,
+       confirmed TIMESTAMP,
+       decided TIMESTAMP);
+
+CREATE SEQUENCE MemberAppSeq START 1;
+
+CREATE TABLE AppVote(
+       app INTEGER NOT NULL,
+       usr INTEGER NOT NULL,
+       PRIMARY KEY (app, usr),
+       FOREIGN KEY (app) REFERENCES MemberApp(id) ON DELETE CASCADE,
+       FOREIGN KEY (usr) REFERENCES WebUser(id) ON DELETE CASCADE);
+