!7 sS passwordSalt() { ret loadTextFileOrCreateWithRandomID(javaxSecretDir("password-salt")); } concept User { S name; SecretValue passwordMD5; S contact; // e.g. mail address bool isMaster; toString { ret or2(name, super.toString()); } } extend AuthedDialogID { new Ref user; // who is logged in bool userMode; // show things as if user was not master } concept UserCreatedObject { new Ref creator; //S nameInRepo; // unique by user, for URL bool isPublic = true; } concept UserPost > UserCreatedObject { S type, title, text; S text() { ret text; } sS _fieldOrder = "isPublic text type title"; } concept PostReference { new Ref post; new Ref referenced; S as; } !include once #1029928 // new Compact Module include set flag NoNanoHTTPD. /*c*/module GazelleExamples > DynNewBot2 { void start { botName = heading = adminName = "gazelle.rocks"; set enableUsers; set useWebSockets; set showRegisterLink; unset showTalkToBotLink; set alwaysRedirectToHttps; passwordSalt(); // make early super.start(); db_mainConcepts().modifyOnCreate = true; indexConceptFieldCI(User, "name"); } L crudClasses(Req req) { L l = super.crudClasses(req); l.add(UserPost); if (req.masterAuthed) l.add(User); ret l; } S serveRegisterForm(SS params) { S user = trim(params.get("user")); S pw = trim(params.get("f_pw")); S pw2 = trim(params.get("pw2")); S redirect = params.get("redirect"); S contact = trim(params.get("contact")); redirect = dropParamFromURL(redirect, "logout"); // don't log us out right again if (empty(redirect)) redirect = baseLink + "/"; new LS msgs; if (nempty(user) || nempty(pw)) { lock dbLock(); if (empty(user)) msgs.add("Please enter a user name"); else if (l(user) < 4) msgs.add("Minimum user name length: 4 characters"); else if (l(user) > 30) msgs.add("Maximum user name length: 30 characters"); else if (hasConceptIC User(name := user)) msgs.add("This user exists, please choose a different name"); if (regexpContainsIC("[^a-z0-9\\-\\.]", user)) msgs.add("Bad characters in user name (please use only a-z, 0-9, - and .)"); if (empty(pw)) msgs.add("Please enter a password"); else if (l(pw) < 6) msgs.add("Minimum password length: 6 characters"); if (neq(pw, pw2)) msgs.add("Passwords don't match"); if (empty(msgs)) { cnew User(name := user, passwordMD5 := SecretValue(hashPW(pw)), +contact); ret hrefresh(5.0, redirect) + "User " + user + " created! Redirecting..."; } } ret hhtml(hhead(htitle("Register new user") + hsansserif() + hmobilefix() + hresponstable()) + hbody(hfullcenter( h3_htmlEncode(adminName + " | Register new user") + hpostform( hhidden(+redirect) + tag table( (empty(msgs) ? "" : tr(td() + td() + td(htmlEncode2_nlToBr(lines_rtrim(msgs))))) + tr(td_valignTop("Choose a user name:") + td_valignTop(hinputfield(+user) + "
" + "(Minimum length 4. Characters allowed: a-z, 0-9, - and .)")) + tr(td_valignTop("Choose a password:") + td_valignTop(hpassword(f_pw := pw) + "
" + "(Minimum length 6 characters)")) + tr(td_valignTop("Repeat the password please:") + td(hpassword(+pw2))) + tr(td_valignTop("Way to contact you (e.g. e-mail) - optional:") + td(hpassword(+contact))) + tr(td() + td(hsubmit("Register"))), class := "responstable"), action := baseLink + "/register") ))); } bool calcMasterAuthed(Req req) { ret super.calcMasterAuthed(req) || req.auth != null && req.auth.user.has() && req.auth.user->isMaster; } O serveOtherPage(Req req) { S uri = req.uri; new Matches m; if (eq(uri, "/register")) ret serveRegisterForm(req.params); if (eq(uri, "/becomeMaster") && req.auth != null && req.auth.user != null) { S pw = trim(req.params.get("masterPW")); if (eq(pw, realPW())) { cset(req.auth.user, isMaster := true); ret "You are master, " + req.auth.user + "!"; } if (nempty(pw)) ret "Bad master PW"; ret hhtml(hhead(htitle("Become master") + hsansserif() + hmobilefix() + hresponstable()) + hbody(hfullcenter( h3_htmlEncode(adminName + " | Become master") + hpostform( tag table( tr(td_valignTop("You are:") + td_valignTop(htmlEncode2(req.auth.user->name))) + tr(td_valignTop("Enter the master password:") + td_valignTop(hpassword(masterPW := pw))) + tr(td() + td(hsubmit("Become master user"))), class := "responstable"), action := baseLink + "/becomeMaster") ))); } if (swic(uri, "/text/", m)) { long id = parseLong(m.rest()); UserPost post = getConceptOpt UserPost(id); if (post == null) ret serve404("Post with ID " + id + " not found"); if (!post.isPublic) ret serve404("Post is not public"); ret serveText(post.text()); } S uri2 = dropSlashPrefix(uri); if (isInteger(uri2)) { long id = parseLong(uri2); UserPost post = getConceptOpt UserPost(id); if (post == null) ret serve404("Post with ID " + id + " not found"); ret servePost(post); } try object serveOtherPage2(req); ret super.serveOtherPage(req); } !include #1029962 // serveOtherPage2 O servePost(UserPost post) { if (!post.isPublic) ret serve404("Post is not public"); new HTMLFramer1 framer; framer.title = post.title; framer.add(p("By " + (post.creator.has() ? htmlEncode2(post.creator->name) : "?") + ". " + renderConceptDate(post))); if (nempty(post.type)) framer.add(p("Post type: "+ htmlEncode2(post.type))); framer.add(hsourcecode(post.text)); ret framer.render(); } S serveAuthForm(S redirect) { ret hAddToHead(super.serveAuthForm(redirect), hInitWebSocket()); } // handle user log-in (create/update AuthedDialogID concept) O handleAuth(Req req, S cookie) null { S name = trim(req.params.get('user)), pw = trim(req.params.get('pw)); if (nempty(name) && nempty(pw) && nempty(cookie)) { Domain authDomain; User user = conceptWhereCI User(+name); if (user == null) ret errorMsg("User " + htmlEncode2(name) + " not found"); if (!eq(getVar(user.passwordMD5), hashPW(pw))) ret errorMsg("Bad password, please try again"); // auth cset(uniq AuthedDialogID(+cookie), restrictedToDomain := null, master := user.isMaster, +user); S redirect = req.params.get('redirect); if (nempty(redirect)) ret hrefresh(redirect); } } O serve2(Req req) { S userMode = req.params.get("userMode"); if (nempty(userMode) && req.auth != null) req.auth.userMode = eq(userMode, "1"); ret super.serve2(req); } S hashPW(S pw) { ret md5(pw + passwordSalt()); } bool inMasterMode(Req req) { ret req.masterAuthed && !req.auth.userMode; } MapSO filtersForClass(Class c, Req req) { if (c == UserPost) { if (!inMasterMode(req)) ret litmap(creator := req.auth.user!); } ret super.filtersForClass(c, req); } S loggedInUserDesc(Req req) { ret req.auth == null ? "not logged in" : req.auth.user! == null ? (req.masterAuthed ? "master user" : "weird user") : "user " + req.auth.user->name; } HCRUD_Concepts crudData(Class c) { HCRUD_Concepts cc = super.crudData(c); if (c == UserPost) { cc.addRenderer("text", new HCRUD_Data.TextArea(80, 10)); cc.fieldHelp( type := "Type of post (any format, optional)", title := "Title for this post (any format, optional)", text := "Text contents of this post (any format)", nameInRepo := "Name in repository (not really used yet)"); cc.massageItemMapForList = (item, map) -> { applyFunctionToValue shorten(map, "text", "title", "type", "nameInRepo"); }; } ret cc; } HCRUD makeCRUD(Class c, Req req) { HCRUD crud = super.makeCRUD(c, req); if (c == UserPost) crud.renderCmds = map -> { UserPost post = getConcept UserPost(toLong(crud.itemID(map))); ret joinNemptiesWithVBar(crud.renderCmds_base(map), targetBlank(postLink(post), "Show post")); }; if (c == User) crud.uneditableFields = litset("passwordMD5"); ret crud; } S authFormHeading() { ret h3_htmlEncode("Welcome to the Gazelle AI System") + p(hsnippetimg_scaleToWidth(200, #1101482, 425, 257, title := "Gazelle")); } void makeFramer(Req req) { super.makeFramer(req); if (req.masterAuthed) req.framer.add(p_vbar( ahrefIf(req.auth.userMode, appendQueryToURL(baseLink + req.uri, userMode := 0), "Master mode", title := "View pages as master"), ahrefIf(!req.auth.userMode, appendQueryToURL(baseLink + req.uri, userMode := 1), "User mode", title := "View pages as user"))); } // link to show post S postLink(UserPost post) { ret baseLink + "/" + post.id; } } // end of module