!7 // trying to move to /admin, but that's complicated sS passwordSalt() { ret loadTextFileOrCreateWithRandomID(javaxSecretDir("password-salt")); } concept PostReferenceable {} concept User > PostReferenceable { S name; SecretValue passwordMD5; S contact; // e.g. mail address bool isMaster; SecretValue botToken = aSecretGlobalID/*UnlessLoading*/(); toString { ret nempty(name) ? "User " + name : super.toString(); } } extend AuthedDialogID { new Ref user; // who is logged in bool userMode; // show things as if user was not master } concept UserCreatedObject > PostReferenceable { new Ref creator; S botInfo; // info on how this was made //S nameInRepo; // unique by user, for URL } concept UserPost > UserCreatedObject { S type, title, text; bool isPublic = true; S text() { ret text; } new RefL postRefs; new LS postRefTags; sS _fieldOrder = "isPublic text type title postRefs"; toString { ret "Post by " + userName(creator!) + ": " + (nempty(title) ? shorten(title) : shorten(text)); } } /*concept BotHandle > UserCreatedObject { S globalID = aGlobalIDUnlessLoading(); S comment; SecretValue token; }*/ !include once #1029928 // new Compact Module include set flag NoNanoHTTPD. /*c*/module GazelleExamples > DynNewBot2 { void start { baseLink = "/admin"; 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 = or2(post.title, shorten(post.text)); 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)); // show render link if (eqic(post.type, "HTML")) framer.add(p(ahref("/html/" + post.id, "Show as HTML page"))); // show edit link if (canEditPost(post)) framer.add(p(ahref(conceptEditLink(post), "Edit post"))); // show reply link framer.add(p(ahref(replyLink(post), "Reply to this post"))); // show post refs L postRefs = cloneList(post.postRefs); if (nempty(postRefs)) { framer.add(p("In reference to:")); framer.add(ul(cloneMap(postRefs, ref -> ahref(objectLink(ref), htmlEncode_nlToBr_withIndents(str(ref)))))); } // show referencing posts Cl refs = sortedByConceptID(instancesOf UserPost(allBackRefs(post))); if (nempty(refs)) { framer.add(p("Referenced by posts:")); framer.add(ul(lmap objectToHTML(refs))); } 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, Req req) { HCRUD_Concepts cc = super.crudData(c, req); 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)", botInfo := "Info on which bot made this post (if any)"); cc.massageItemMapForList = (item, map) -> { applyFunctionToValue shorten(map, "text", "title", "type", "nameInRepo"); }; cc.emptyObject = () -> { MapSO item = cc.emptyObject_base(); item.put(creator := req.auth.user!); // set default creator to current user if in master mode ret item; }; } 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")); }; crud.uneditableFields = litset("postRefTags"); } if (c == User) { crud.allowCreate = false; 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 objectLink(post); } S objectLink(Concept c) { ret baseLink + "/" + c.id; } S objectToHTML(Concept c) { ret ahref(objectLink(c), htmlEncode_nlToBr_withIndents(str(c))); } bool canEditPost(UserPost post) { Req req = currentReq!; ret req != null && req.auth != null && req.auth.user! != null && (req.auth.user->isMaster || req.auth.user! == post.creator!); } S replyLink(UserPost post) { ret appendQueryToURL(crudLink(UserPost), cmd := "new", fList_postRefs_0 := post.id); } } // end of module sS userName(User user) { ret user != null ? user.name : "?"; }