Skip to content

Commit 701234a

Browse files
committed
wires up login request handlers
1 parent a94bc95 commit 701234a

File tree

4 files changed

+114
-7
lines changed

4 files changed

+114
-7
lines changed

src/FsTweet.Web/Auth.fs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace Auth
33
module Domain =
44
open User
55
open Chessie.ErrorHandling
6+
open Chessie
67

78
type LoginRequest = {
89
Username : Username
@@ -26,6 +27,24 @@ module Domain =
2627

2728
type Login = FindUser -> LoginRequest -> AsyncResult<User, LoginError>
2829

30+
let login (findUser : FindUser) (req : LoginRequest) = asyncTrial {
31+
let! userToFind =
32+
findUser req.Username |> AR.mapFailure Error
33+
match userToFind with
34+
| None ->
35+
return! AR.fail UsernameNotFound
36+
| Some user ->
37+
match user.Email with
38+
| NotVerified _ ->
39+
return! AR.fail EmailNotVerified
40+
| Verified _ ->
41+
let isMatchingPassword =
42+
PasswordHash.VerifyPassword req.Password user.PasswordHash
43+
match isMatchingPassword with
44+
| false -> return! AR.fail PasswordMisMatch
45+
| _ -> return user
46+
}
47+
2948
module Suave =
3049
open Suave
3150
open Suave.Filters
@@ -35,6 +54,7 @@ module Suave =
3554
open Domain
3655
open Chessie.ErrorHandling
3756
open Chessie
57+
open User
3858

3959
type LoginViewModel = {
4060
Username : string
@@ -48,14 +68,49 @@ module Suave =
4868
Error = None
4969
}
5070

51-
let handleUserLogin ctx = async {
71+
let onLoginSuccess (user : User) =
72+
Successful.OK user.Username.Value
73+
74+
let onLoginFailure viewModel loginError =
75+
match loginError with
76+
| PasswordMisMatch ->
77+
let vm =
78+
{viewModel with Error = Some "password didn't match"}
79+
page "guest/login.liquid" vm
80+
| EmailNotVerified ->
81+
let vm =
82+
{viewModel with Error = Some "email not verified"}
83+
page "guest/login.liquid" vm
84+
| UsernameNotFound ->
85+
let vm =
86+
{viewModel with Error = Some "invalid username"}
87+
page "guest/login.liquid" vm
88+
| Error ex ->
89+
printfn "%A" ex
90+
let vm =
91+
{viewModel with Error = Some "something went wrong"}
92+
page "guest/login.liquid" vm
93+
94+
let handleLoginResult viewModel loginResult =
95+
either onLoginSuccess (onLoginFailure viewModel) loginResult
96+
97+
let handleLoginAsyncResult viewModel aLoginResult =
98+
aLoginResult
99+
|> Async.ofAsyncResult
100+
|> Async.map (handleLoginResult viewModel)
101+
102+
103+
let handleUserLogin findUser ctx = async {
52104
match bindEmptyForm ctx.request with
53105
| Choice1Of2 (vm : LoginViewModel) ->
54106
let result =
55107
LoginRequest.TryCreate (vm.Username, vm.Password)
56108
match result with
57109
| Success req ->
58-
return! Successful.OK "TODO" ctx
110+
let aLoginResult = login findUser req
111+
let! webpart =
112+
handleLoginAsyncResult vm aLoginResult
113+
return! webpart ctx
59114
| Failure err ->
60115
let viewModel = {vm with Error = Some err}
61116
return! page "guest/login.liquid" viewModel ctx
@@ -67,8 +122,9 @@ module Suave =
67122

68123
let renderLoginPage viewModel =
69124
page "guest/login.liquid" viewModel
70-
let webpart () =
125+
let webpart getDataCtx =
126+
let findUser = Persistence.findUser getDataCtx
71127
path "/login" >=> choose [
72128
GET >=> renderLoginPage emptyLoginViewModel
73-
POST >=> handleUserLogin
129+
POST >=> handleUserLogin findUser
74130
]

src/FsTweet.Web/Chessie.fs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,10 @@ module AR =
3131
aComputation
3232
|> Async.Catch
3333
|> Async.map ofChoice
34-
|> AR
34+
|> AR
35+
36+
let fail x =
37+
x
38+
|> fail
39+
|> Async.singleton
40+
|> AR

src/FsTweet.Web/FsTweet.Web.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ let main argv =
5454
serveAssets
5555
path "/" >=> page "guest/home.liquid" ""
5656
UserSignup.Suave.webPart getDataCtx sendEmail
57-
Auth.Suave.webpart ()
57+
Auth.Suave.webpart getDataCtx
5858
]
5959

6060
startWebServer defaultConfig app

src/FsTweet.Web/User.fs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ type PasswordHash = private PasswordHash of string with
5151
BCrypt.HashPassword(password.Value)
5252
|> PasswordHash
5353

54+
static member TryCreate passwordHash =
55+
try
56+
BCrypt.InterrogateHash passwordHash |> ignore
57+
PasswordHash passwordHash |> ok
58+
with
59+
| _ -> fail "Invalid Password Hash"
60+
61+
static member VerifyPassword (password : Password) (passwordHash : PasswordHash) =
62+
BCrypt.Verify(password.Value, passwordHash.Value)
63+
5464
type UserEmail =
5565
| Verified of EmailAddress
5666
| NotVerified of EmailAddress
@@ -67,4 +77,39 @@ type FindUser = Username -> AsyncResult<User option, System.Exception>
6777

6878
module Persistence =
6979
open Database
70-
let findUser (getDataCtx : GetDataContext) (username : Username) = ()
80+
open FSharp.Data.Sql
81+
open Chessie
82+
let mapUser (user : DataContext.``public.UsersEntity``) =
83+
let userResult = trial {
84+
let! username = Username.TryCreate user.Username
85+
let! passwordHash = PasswordHash.TryCreate user.PasswordHash
86+
let! email = EmailAddress.TryCreate user.Email
87+
let userEmail =
88+
match user.IsEmailVerified with
89+
| true -> Verified email
90+
| _ -> NotVerified email
91+
return {
92+
UserId = UserId user.Id
93+
Username = username
94+
PasswordHash = passwordHash
95+
Email = userEmail
96+
}
97+
}
98+
userResult
99+
|> mapFailure (System.Exception)
100+
|> Async.singleton
101+
|> AR
102+
let findUser (getDataCtx : GetDataContext) (username : Username) = asyncTrial {
103+
let ctx = getDataCtx()
104+
let! userToFind =
105+
query {
106+
for u in ctx.Public.Users do
107+
where (u.Username = username.Value)
108+
} |> Seq.tryHeadAsync |> AR.catch
109+
match userToFind with
110+
| Some user ->
111+
let! user = mapUser user
112+
return Some user
113+
| None -> return None
114+
}
115+

0 commit comments

Comments
 (0)