adam@16: functor Make(M: sig adam@16: con cols :: {Type} adam@16: constraint [Id] ~ cols adam@17: val folder : folder cols adam@17: val inj : $(map sql_injectable cols) adam@17: adam@17: type creationState adam@17: type creationData adam@17: val creationState : transaction creationState adam@17: val render : creationState -> xtable adam@17: val tabulate : creationState -> signal creationData adam@17: val choose : sql_table ([Id = string] ++ cols) [Pkey = [Id]] -> creationData -> transaction $cols adam@16: adam@16: val sessionLifetime : int adam@16: val afterLogout : url adam@16: val secureCookies : bool adam@16: val association : Openid.association_mode adam@16: val realm : option string adam@17: val formClass : css_class adam@16: end) = struct adam@16: adam@16: type user = string adam@16: val show_user = _ adam@16: val inj_user = _ adam@16: adam@16: table user : ([Id = user] ++ M.cols) adam@16: PRIMARY KEY Id adam@16: adam@16: table identity : {User : user, Identifier : string} adam@16: PRIMARY KEY (User, Identifier) adam@16: adam@16: sequence sessionIds adam@16: adam@16: table session : {Id : int, Key : int, Identifier : option string, Expires : time} adam@16: PRIMARY KEY Id adam@16: adam@16: cookie signingUp : {Session : int, Key : int} adam@16: cookie login : {User : user, Session : int, Key : int} adam@16: adam@17: val currentUrl = adam@17: b <- currentUrlHasPost; adam@17: if b then adam@17: return M.afterLogout adam@17: else adam@17: currentUrl adam@17: adam@16: val current = adam@16: login <- getCookie login; adam@16: case login of adam@16: None => return None adam@16: | Some login => adam@16: ident <- oneOrNoRowsE1 (SELECT (session.Identifier) adam@16: FROM session adam@16: WHERE session.Id = {[login.Session]} adam@16: AND session.Key = {[login.Key]}); adam@16: case ident of adam@18: None => return None adam@16: | Some None => return None adam@16: | Some (Some ident) => adam@16: valid <- oneRowE1 (SELECT COUNT( * ) > 0 adam@16: FROM identity adam@16: WHERE identity.User = {[login.User]} adam@16: AND identity.Identifier = {[ident]}); adam@16: if valid then adam@16: return (Some login.User) adam@16: else adam@16: error Session not authorized to act as user adam@16: adam@17: fun validUser s = String.length s > 0 && String.length s < 20 adam@17: && String.all Char.isAlnum s adam@17: adam@16: fun main wrap = adam@16: let adam@16: fun logout () = adam@16: clearCookie login; adam@16: redirect M.afterLogout adam@16: adam@17: fun signupDetails after = adam@17: let adam@17: fun finishSignup uid data = adam@17: if not (validUser uid) then adam@17: return (Some "That username is not valid. It must be between 1 and 19 characters long, containing only letters and numbers.") adam@17: else adam@17: used <- oneRowE1 (SELECT COUNT( * ) > 0 adam@17: FROM user adam@17: WHERE user.Id = {[uid]}); adam@17: if used then adam@17: return (Some "That username is taken. Please choose another.") adam@17: else adam@17: ses <- getCookie signingUp; adam@17: case ses of adam@17: None => return (Some "Missing session cookie") adam@17: | Some ses => adam@17: ident <- oneOrNoRowsE1 (SELECT (session.Identifier) adam@17: FROM session adam@17: WHERE session.Id = {[ses.Session]} adam@17: AND session.Key = {[ses.Key]}); adam@17: case ident of adam@17: None => return (Some "Invalid session data") adam@17: | Some None => return (Some "Session has no associated identifier") adam@17: | Some (Some ident) => adam@17: clearCookie signingUp; adam@17: setCookie login {Value = {User = uid} ++ ses, adam@17: Expires = None, adam@17: Secure = M.secureCookies}; adam@17: adam@17: cols <- M.choose user data; adam@17: dml (insert user ({Id = (SQL {[uid]})} ++ @Sql.sqexps M.folder M.inj cols)); adam@17: dml (INSERT INTO identity (User, Identifier) adam@17: VALUES ({[uid]}, {[ident]})); adam@17: redirect (bless after) adam@17: in adam@17: uid <- source ""; adam@17: cs <- M.creationState; adam@17: adam@17: wrap "Your User Details" adam@17: adam@17: adam@17: {M.render cs} adam@17: adam@17:
Username:
adam@17:
adam@17: end adam@17: adam@16: fun opCallback after ses res = adam@16: case res of adam@16: Openid.Canceled => error You canceled the login process. adam@16: | Openid.Failure s => error Login failed: {[s]} adam@16: | Openid.AuthenticatedAs ident => adam@16: signup <- getCookie signingUp; adam@16: case signup of adam@16: Some signup => adam@16: if signup.Session <> ses then adam@16: error Session has changed suspiciously adam@16: else adam@16: invalid <- oneRowE1 (SELECT COUNT( * ) = 0 adam@16: FROM session adam@16: WHERE session.Id = {[signup.Session]} adam@16: AND session.Key = {[signup.Key]}); adam@16: if invalid then adam@16: error Invalid or expired session adam@16: else adam@17: dml (UPDATE session adam@17: SET Identifier = {[Some ident]} adam@17: WHERE Id = {[signup.Session]}); adam@17: signupDetails after adam@16: | None => adam@16: login <- getCookie login; adam@16: case login of adam@16: None => error Missing session cookie adam@16: | Some login => adam@16: if login.Session <> ses then adam@16: error Session has changed suspiciously adam@16: else adam@16: invalid <- oneRowE1 (SELECT COUNT( * ) = 0 adam@16: FROM session adam@16: WHERE session.Id = {[login.Session]} adam@16: AND session.Key = {[login.Key]}); adam@16: if invalid then adam@16: error Invalid or expired session adam@16: else adam@16: dml (UPDATE session adam@16: SET Identifier = {[Some ident]} adam@17: WHERE Id = {[login.Session]}); adam@16: redirect (bless after) adam@16: adam@16: fun newSession () = adam@16: ses <- nextval sessionIds; adam@16: now <- now; adam@16: key <- rand; adam@16: dml (INSERT INTO session (Id, Key, Identifier, Expires) adam@16: VALUES ({[ses]}, {[key]}, NULL, {[addSeconds now M.sessionLifetime]})); adam@16: return {Session = ses, Key = key} adam@16: adam@17: fun logon after r = adam@16: ident <- oneOrNoRowsE1 (SELECT (identity.Identifier) adam@16: FROM identity adam@16: WHERE identity.User = {[r.User]} adam@16: LIMIT 1); adam@16: case ident of adam@16: None => error Username not found adam@16: | Some ident => adam@16: ses <- newSession (); adam@16: setCookie login {Value = r ++ ses, adam@16: Expires = None, adam@16: Secure = M.secureCookies}; adam@16: ses <- return ses.Session; adam@16: msg <- Openid.authenticate (opCallback after ses) adam@16: {Association = M.association, adam@16: Realm = M.realm, adam@16: Identifier = ident}; adam@16: error Login with your identity provider failed: {[msg]} adam@16: adam@16: fun doSignup after r = adam@16: ses <- newSession (); adam@16: setCookie signingUp {Value = ses, adam@16: Expires = None, adam@16: Secure = M.secureCookies}; adam@16: ses <- return ses.Session; adam@16: msg <- Openid.authenticate (opCallback after ses) adam@16: {Association = M.association, adam@16: Realm = M.realm, adam@16: Identifier = r.Identifier}; adam@16: error Login with your identity provider failed: {[msg]} adam@16: adam@16: fun signup () = adam@16: after <- currentUrl; adam@16: wrap "Account Signup" adam@16:
adam@16: OpenID Identifier:
adam@16: adam@16: adam@16:
adam@16: in adam@16: cur <- current; adam@17: here <- currentUrl; adam@16: case cur of adam@16: Some cur => return Logged in as {[cur]}. [Log out] adam@16: | None => return adam@17:
adam@16: Sign up adam@16:
adam@16: end adam@16: adam@16: task periodic 60 = fn () => dml (DELETE FROM session adam@16: WHERE Expires >= CURRENT_TIMESTAMP) adam@16: adam@16: end