diff --git a/nimforum.nimble b/nimforum.nimble index 4a03209..f28eaaf 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -15,6 +15,7 @@ skipExt = @["nim"] requires "nim >= 0.14.0" requires "jester#64295c8" requires "bcrypt#head" +requires "hmac#9c61ebe2fd134cf97" requires "recaptcha 1.0.2" requires "sass" diff --git a/src/auth.nim b/src/auth.nim index 20e0b74..0b08bfe 100644 --- a/src/auth.nim +++ b/src/auth.nim @@ -1,6 +1,6 @@ import random, md5 -import bcrypt +import bcrypt, hmac proc randomSalt(): string = result = "" @@ -47,12 +47,44 @@ proc makePassword*(password, salt: string, comparingTo = ""): string = let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8) result = hash(getMD5(salt & getMD5(password)), bcryptSalt) -proc makeIdentHash*(user, password: string, epoch: int64, secret: string, - comparingTo = ""): string = +proc makeIdentHash*(user, password: string, epoch: int64, + secret: string): string = ## Creates a hash verifying the identity of a user. Used for password reset ## links and email activation links. ## The ``epoch`` determines the creation time of this hash, it will be checked ## during verification to ensure the hash hasn't expired. ## The ``secret`` is the 'salt' field in the ``person`` table. - let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8) - result = hash(user & password & $epoch & secret, bcryptSalt) \ No newline at end of file + result = hmac_sha256(secret, user & password & $epoch).toHex() + + +when isMainModule: + block: + let ident = makeIdentHash("test", "pass", 1526908753, "randomtext") + let ident2 = makeIdentHash("test", "pass", 1526908753, "randomtext") + doAssert ident == ident2 + + let invalid = makeIdentHash("test", "pass", 1526908754, "randomtext") + doAssert ident != invalid + + block: + let ident = makeIdentHash( + "test", + "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", + 1526908753, + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + ) + let ident2 = makeIdentHash( + "test", + "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", + 1526908753, + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + ) + doAssert ident == ident2 + + let invalid = makeIdentHash( + "test", + "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", + 1526908754, + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + ) + doAssert ident != invalid \ No newline at end of file diff --git a/src/forum.nim b/src/forum.nim index 472826c..5c6516f 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -206,7 +206,7 @@ proc verifyIdentHash( var row = getRow(db, query, name) if row[0] == "": raise newForumError("User doesn't exist.", @["nick"]) - let newIdent = makeIdentHash(name, row[0], epoch, row[1], ident) + let newIdent = makeIdentHash(name, row[0], epoch, row[1]) # Check that it hasn't expired. let diff = getTime() - epoch.fromUnix() if diff.hours > 2: @@ -1250,7 +1250,7 @@ routes: except ForumError as exc: resp Http400, $(%exc.data),"application/json" - get "/activateEmail": + post "/activateEmail": createTFD() cond(@"nick" != "") cond(@"epoch" != "") @@ -1267,9 +1267,9 @@ routes: """, $Rank.Moderated, @"nick" ) - redirect(uri("/activateEmail/success")) + resp Http200, "{}", "application/json" except ForumError as exc: - redirect(uri("/activateEmail/failure/" & encodeUrl(exc.data.message))) + resp Http400, $(%exc.data),"application/json" get "/t/@id": cond "id" in request.params diff --git a/src/frontend/activateemail.nim b/src/frontend/activateemail.nim new file mode 100644 index 0000000..4b049da --- /dev/null +++ b/src/frontend/activateemail.nim @@ -0,0 +1,58 @@ +when defined(js): + import sugar, httpcore, options, json + import dom except Event + + include karax/prelude + import karax / [kajax, kdom] + + import error, replybox, threadlist, post + import karaxutils + + type + ActivateEmail* = ref object + loading: bool + status: HttpCode + error: Option[PostError] + newPassword: kstring + + proc newActivateEmail*(): ActivateEmail = + ActivateEmail( + status: Http200, + newPassword: "" + ) + + proc onPassChange(e: Event, n: VNode, state: ActivateEmail) = + state.newPassword = n.value + + proc onPost(httpStatus: int, response: kstring, state: ActivateEmail) = + postFinished: + navigateTo(makeUri("/activateEmail/success")) + + proc onSetClick( + ev: Event, n: VNode, + state: ActivateEmail + ) = + state.loading = true + state.error = none[PostError]() + + let uri = makeUri("activateEmail", search = $kdom.window.location.search) + ajaxPost(uri, @[], "", + (s: int, r: kstring) => onPost(s, r, state)) + + proc render*(state: ActivateEmail): VNode = + result = buildHtml(): + section(class="container grid-xl"): + tdiv(id="activateemail"): + tdiv(class="title"): + p(): text "Activate Email" + tdiv(class="content"): + button(class=class( + {"loading": state.loading}, + "btn btn-primary" + ), + onClick=(ev: Event, n: VNode) => + (onSetClick(ev, n, state))): + text "Activate" + if state.error.isSome(): + p(class="text-error"): + text state.error.get().message \ No newline at end of file diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index e892549..5a06083 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -5,7 +5,7 @@ include karax/prelude import jester/patterns import threadlist, postlist, header, profile, newthread, error, about -import resetpassword +import resetpassword, activateemail import karaxutils type @@ -15,6 +15,7 @@ type newThread: NewThread about: About resetPassword: ResetPassword + activateEmail: ActivateEmail proc copyLocation(loc: Location): Location = # TODO: It sucks that I had to do this. We need a nice way to deep copy in JS. @@ -35,7 +36,8 @@ proc newState(): State = profile: newProfileState(), newThread: newNewThread(), about: newAbout(), - resetPassword: newResetPassword() + resetPassword: newResetPassword(), + activateEmail: newActivateEmail() ) var state = newState() @@ -104,13 +106,9 @@ proc render(): VNode = ) ) ), - r("/activateEmail/failure/@msg", + r("/activateEmail", (params: Params) => ( - renderMessage( - "Email activation failed", - decodeUrl(params["msg"]), - "fa-exclamation" - ) + render(state.activateEmail) ) ), r("/resetPassword/success",