diff --git a/src/forum.nim b/src/forum.nim index 6c423b0..86b95d0 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -1144,6 +1144,19 @@ proc executeLogin(c: TForumData, username, password: string): string = raise newForumError("Invalid username or password") +proc sendEmailActivation(c: TForumData, name, password, + email, salt: string) {.async.} = + let epoch = $int(epochTime()) + let activateUrl = c.req.makeUri("/activateEmail?nick=$1&epoch=$2&ident=$3" % + [encodeUrl(name), encodeUrl(epoch), + encodeUrl(makeIdentHash(name, password, epoch, salt))]) + + let emailSentFut = sendEmailActivation(c.config, email, name, activateUrl) + yield emailSentFut + if emailSentFut.failed: + echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg) + raise newForumError("Couldn't send activation email", @["email"]) + proc executeRegister(c: TForumData, name, pass, antibot, userIp, email: string): Future[string] {.async.} = ## Registers a new user and returns a new session key for that user's @@ -1158,7 +1171,7 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, raise newForumError("Email already exists", @["email"]) # Username validation: - if name.len == 0 or not allCharsInSet(name, UsernameIdent): + if name.len == 0 or not allCharsInSet(name, UsernameIdent) or name.len > 20: raise newForumError("Invalid username", @["username"]) if getValue(db, sql"select name from person where name = ?", name).len > 0: raise newForumError("Username already exists", @["username"]) @@ -1181,16 +1194,7 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, let password = makePassword(pass, salt) # Send activation email. - let epoch = $int(epochTime()) - let activateUrl = c.req.makeUri("/activateEmail?nick=$1&epoch=$2&ident=$3" % - [encodeUrl(name), encodeUrl(epoch), - encodeUrl(makeIdentHash(name, password, epoch, salt))]) - - let emailSentFut = sendEmailActivation(c.config, email, name, activateUrl) - yield emailSentFut - if emailSentFut.failed: - echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg) - raise newForumError("Couldn't send activation email", @["email"]) + await sendEmailActivation(c, name, password, email, salt) # Add account to person table exec(db, sql""" @@ -1254,6 +1258,42 @@ proc executeDeleteThread(c: TForumData, threadId: int) = # Set the `isDeleted` flag. exec(db, crud(crUpdate, "thread", "isDeleted"), "1", threadId) +proc executeDeleteUser(c: TForumData, username: string) = + # Verify that the current user has the permissions to do this. + if username != c.username and c.rank < Admin: + raise newForumError("You cannot delete this user.") + + # Set the `isDeleted` flag. + exec(db, sql"update person set isDeleted = 1 where name = ?;", username) + +proc updateProfile( + c: TForumData, username, email: string, rank: Rank +) {.async.} = + if c.rank < rank: + raise newForumError("You cannot set a rank that is higher than yours.") + + if c.username != username and c.rank < Moderator: + raise newForumError("You can't change this profile.") + + # Make sure the rank is set to EmailUnconfirmed when the email changes. + if c.rank < Moderator: + let row = getRow( + db, + sql"select name, password, email, salt from person where name = ?", + username + ) + if row[2] != email: + if rank != EmailUnconfirmed: + raise newForumError("Rank needs a change when setting new email.") + + await sendEmailActivation(c, row[0], row[1], row[2], row[3]) + + exec( + db, + sql"update person set status = ?, email = ? where name = ?;", + $rank, email, username + ) + initialise() routes: @@ -1706,6 +1746,51 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post re"/deleteUser": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "username" in formData + + let username = formData["username"].body + + try: + executeDeleteUser(c, username) + resp Http200, "{}", "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + + post re"/saveProfile": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "username" in formData + cond "email" in formData + cond "rank" in formData + + let username = formData["username"].body + let email = formData["email"].body + let rank = parseEnum[Rank](formData["rank"].body) + + try: + await updateProfile(c, username, email, rank) + resp Http200, "{}", "application/json" + except ForumError: + let exc = (ref ForumError)(getCurrentException()) + resp Http400, $(%exc.data), "application/json" + get "/t/@id": cond "id" in request.params diff --git a/src/frontend/profile.nim b/src/frontend/profile.nim index 322e0ec..56e0d43 100644 --- a/src/frontend/profile.nim +++ b/src/frontend/profile.nim @@ -35,7 +35,8 @@ when defined(js): let profile = to(parsed, Profile) state.profile = some(profile) - state.settings = some(newProfileSettings(profile)) + if profile.email.isSome(): + state.settings = some(newProfileSettings(profile)) proc genPostLink(link: PostLink): VNode = let url = renderPostUrl(link) diff --git a/src/frontend/profilesettings.nim b/src/frontend/profilesettings.nim index 6884df9..382d11e 100644 --- a/src/frontend/profilesettings.nim +++ b/src/frontend/profilesettings.nim @@ -26,6 +26,8 @@ when defined(js): state.email = profile.email.get() state.rank = profile.user.rank + state.error = none[PostError]() + proc newProfileSettings*(profile: Profile): ProfileSettings = result = ProfileSettings( status: Http200, @@ -38,7 +40,8 @@ when defined(js): proc onProfilePost(httpStatus: int, response: kstring, state: ProfileSettings) = postFinished: - discard + state.profile.email = some($state.email) + state.profile.user.rank = state.rank proc onEmailChange(event: Event, node: VNode, state: ProfileSettings) = state.email = node.value @@ -66,11 +69,12 @@ when defined(js): ajaxPost(uri, @[], cast[cstring](formData), (s: int, r: kstring) => onProfilePost(s, r, state)) + proc needsSave(state: ProfileSettings): bool = + state.email != state.profile.email.get() or + state.rank != state.profile.user.rank + proc render*(state: ProfileSettings, currentUser: Option[User]): VNode = - if state.status != Http200: - return renderError("Couldn't save profile") - let isAdmin = currentUser.isSome() and currentUser.get().rank == Admin let canResetPassword = state.profile.user.rank > EmailUnconfirmed @@ -163,13 +167,21 @@ when defined(js): text " Delete account" tdiv(class="float-right"): - button(class="btn btn-link", + if state.error.isSome(): + span(class="text-error"): + text state.error.get().message + + button(class=class( + {"disabled": not needsSave(state)}, "btn btn-link" + ), onClick=(e: Event, n: VNode) => (resetSettings(state))): text "Cancel" - button(class="btn btn-primary", + button(class=class( + {"disabled": not needsSave(state)}, "btn btn-primary" + ), onClick=(e: Event, n: VNode) => save(state)): - italic(class="fas fa-check") + italic(class="fas fa-save") text " Save" render(state.deleteModal)