Use pwgen instead of apg
[bpt/portal.git] / app / app.sml
CommitLineData
a90da8b1
AC
1structure App :> APP =
2struct
3
a2d53da2 4val baseUrl = "https://join.hcoop.net/join/"
84e42512 5val portalUrl = "https://members2.hcoop.net/portal/"
a90da8b1
AC
6
7open Sql
8
9structure C = PgClient
10
11val db = ref (NONE : C.conn option)
12
13val rnd = ref (Random.rand (0, 0))
14
15fun init () =
16 let
1d2cae17 17 val c = C.conn "dbname='hcoop_hcoop'"
a90da8b1
AC
18 in
19 db := SOME c;
20 C.dml c "BEGIN";
21 rnd := Random.rand (SysWord.toInt (Posix.Process.pidToWord (Posix.ProcEnv.getpid ())),
22 SysWord.toInt (Posix.Process.pidToWord (Posix.ProcEnv.getpid ())))
23 end
24
25fun getDb () = valOf (!db)
26
27fun done () =
28 let
29 val c = getDb ()
30 in
31 C.dml c "COMMIT";
32 C.close c;
33 db := NONE
34 end
35
f3f3ad24
AC
36fun readFile fname =
37 let
38 val inf = TextIO.openIn fname
39
40 fun readLines lines =
41 case TextIO.inputLine inf of
42 NONE => String.concat (List.rev lines)
43 | SOME line => readLines (line :: lines)
44 in
45 readLines []
46 before TextIO.closeIn inf
47 end
48
1d2cae17
AC
49fun readTosBody () = readFile "/home/hcoop/public_html/tos.body.html"
50fun readTosAgree () = readFile "/home/hcoop/public_html/tos.agree.html"
51fun readTosMinorAgree () = readFile "/home/hcoop/public_html/tos.agree.minor.html"
f3f3ad24 52
a90da8b1
AC
53fun sendMail (to, subj, intro, footer, id) =
54 let
d50a0cd7
AC
55 val (name, rname, gname, email, forward, uses, other) =
56 case C.oneOrNoRows (getDb ()) ($`SELECT name, rname, gname, email, forward, uses, other FROM MemberApp WHERE id = ^(C.intToSql id)`) of
57 SOME [name, rname, gname, email, forward, uses, other] =>
f3f3ad24
AC
58 (C.stringFromSql name, C.stringFromSql rname,
59 if C.isNull gname then NONE else SOME (C.stringFromSql gname),
d50a0cd7 60 C.stringFromSql email,
f3f3ad24
AC
61 C.boolFromSql forward, C.stringFromSql uses,
62 C.stringFromSql other)
a90da8b1
AC
63 | _ => raise Fail "Bad sendMail row"
64
65 val proc = Unix.execute ("/usr/sbin/exim4", ["-t"])
66 fun mwrite s = TextIO.output (Unix.textOutstreamOf proc, s)
67 in
6f91863c 68 mwrite ("From: Hcoop Application System <join@hcoop.net>\nTo: ");
a90da8b1
AC
69 mwrite (to);
70 mwrite ("\nSubject: ");
71 mwrite subj;
72 mwrite ("\n\n");
73 mwrite intro;
74 mwrite ("\n\nUsername: ");
75 mwrite (name);
f3f3ad24 76 mwrite ("\nMember real name: ");
a90da8b1 77 mwrite (rname);
f3f3ad24
AC
78 case gname of
79 NONE => ()
80 | SOME gname => (mwrite "\nLegal guardian name: ";
81 mwrite gname);
d50a0cd7
AC
82 mwrite ("\nE-mail address: ");
83 mwrite email;
a90da8b1
AC
84 mwrite ("\nForward e-mail: ");
85 mwrite (if forward then "yes" else "no");
86 mwrite ("\n\nDesired uses:\n");
87 mwrite (uses);
88 mwrite ("\n\nOther information:\n");
89 mwrite (other);
90 mwrite ("\n\n");
91 footer mwrite;
92 OS.Process.isSuccess (Unix.reap proc)
93 end
94
f3f3ad24 95type application = { name : string, rname : string, gname : string option, email : string,
d5f8418b
AC
96 forward : bool, uses : string, other : string,
97 paypal : string option, checkout : string option }
a90da8b1 98
a2d53da2
AC
99fun randomPassword () =
100 let
442f54e7 101 val proc = Unix.execute ("/usr/bin/pwgen", ["-cCnB", "8", "1"])
a2d53da2
AC
102 in
103 case TextIO.inputLine (Unix.textInstreamOf proc) of
442f54e7 104 NONE => raise Fail "Couldn't execute pwgen"
a2d53da2
AC
105 | SOME line =>
106 case String.tokens Char.isSpace line of
107 [s] => s
442f54e7 108 | _ => raise Fail "Couldn't parse output of pwgen"
a2d53da2
AC
109 end
110
d5f8418b
AC
111val allLower = CharVector.map Char.toLower
112
113fun emailToSql so =
114 case so of
115 NONE => "NULL"
116 | SOME s => C.stringToSql (allLower s)
117
118fun apply {name, rname, gname, email, forward, uses, other, paypal, checkout} =
a90da8b1
AC
119 let
120 val db = getDb ()
121 in
122 case C.oneRow db ($`SELECT nextval('MemberAppSeq')`) of
123 [id] =>
124 let
125 val id = C.intFromSql id
126 val passwd = Int.toString (Int.abs (Random.randInt (!rnd)))
a2d53da2 127 val unix_passwd = randomPassword ()
a90da8b1 128 in
d5f8418b
AC
129 C.dml db ($`INSERT INTO MemberApp (id, name, rname, gname, email, forward, uses, other, passwd,
130 status, applied, msg, unix_passwd, paypal, checkout)
a90da8b1 131 VALUES (^(C.intToSql id), ^(C.stringToSql name), ^(C.stringToSql rname),
f3f3ad24 132 ^(case gname of NONE => "NULL" | SOME gname => C.stringToSql gname),
a90da8b1 133 ^(C.stringToSql email), ^(C.boolToSql forward), ^(C.stringToSql uses),
a2d53da2 134 ^(C.stringToSql other), ^(C.stringToSql passwd), 0, CURRENT_TIMESTAMP,
d5f8418b
AC
135 '', ^(C.stringToSql unix_passwd),
136 ^(emailToSql paypal), ^(emailToSql checkout))`);
eea7b531 137 if sendMail (email, "Confirm membership application",
a2d53da2 138 "We've received a request to join the Internet Hosting Cooperative (hcoop.net) with this e-mail address.",
a90da8b1
AC
139 fn mwrite => (mwrite ("To confirm this application, visit ");
140 mwrite (baseUrl);
141 mwrite ("confirm?id=");
142 mwrite (Int.toString id);
143 mwrite ("&p=");
a2d53da2 144 mwrite passwd;
a90da8b1 145 mwrite ("\n")),
eea7b531
AC
146 id) then
147 SOME unix_passwd
148 else
149 NONE
a90da8b1
AC
150 end
151 | _ => raise Fail "Bad next sequence val"
152 end
153
ea8d5198 154fun isIdent ch = Char.isLower ch orelse Char.isDigit ch orelse ch = #"-"
a90da8b1
AC
155
156fun validHost s =
157 size s > 0 andalso size s < 20 andalso List.all isIdent (String.explode s)
158
159fun validDomain s =
160 size s > 0 andalso size s < 100 andalso List.all validHost (String.fields (fn ch => ch = #".") s)
161
162fun validUser s =
163 size s > 0 andalso size s < 50 andalso List.all
164 (fn ch => isIdent ch orelse ch = #"." orelse ch = #"_" orelse ch = #"-" orelse ch = #"+")
165 (String.explode s)
166
167fun validEmailUser s =
168 size s > 0 andalso size s < 50 andalso List.all
169 (fn ch => Char.isAlphaNum ch orelse ch = #"." orelse ch = #"_" orelse ch = #"-" orelse ch = #"+")
170 (String.explode s)
171
172fun validEmail s =
173 (case String.fields (fn ch => ch = #"@") s of
174 [user, host] => validEmailUser user andalso validDomain host
175 | _ => false)
176
177fun userExists name =
178 (Posix.SysDB.getpwnam name; true) handle OS.SysErr _ => false
179
180fun confirm (id, passwd) =
181 let
182 val db = getDb ()
183 in
a2d53da2 184 case C.oneOrNoRows db ($`SELECT unix_passwd FROM MemberApp WHERE id = ^(C.intToSql id) AND passwd = ^(C.stringToSql passwd) AND status = 0`) of
eea7b531 185 SOME [_] =>
a90da8b1 186 (C.dml db ($`UPDATE MemberApp SET status = 1, confirmed = CURRENT_TIMESTAMP WHERE id = ^(C.intToSql id)`);
eea7b531
AC
187 sendMail ("board@hcoop.net",
188 "New membership application",
189 "We've received a new request to join hcoop.",
190 fn mwrite => (mwrite ("Open applications: ");
191 mwrite (portalUrl);
192 mwrite ("apps")),
193 id))
194 | NONE => false
a90da8b1
AC
195 end
196
1d2cae17 197end