all repos

onasty @ 8ffca5c10ae01dced9d3bf97864ce148f90116e5

a one-time notes service
4 files changed, 301 insertions(+), 193 deletions(-)
web: implement sign up (#135)

* web: rename the sign in page to auth

* web: add api client for signup

* web: add sign up/sign in switch to the form
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-06-19 16:15:18 +0300
Parent: 327757c
M web/src/Api/Auth.elm
···
        1
        
        -module Api.Auth exposing (refreshToken, signin)

      
        
        1
        +module Api.Auth exposing (refreshToken, signin, signup)

      
        2
        2
         

      
        3
        3
         import Data.Credentials as Credentials exposing (Credentials)

      
        4
        4
         import Effect exposing (Effect)

      
        5
        5
         import Http

      
        
        6
        +import Json.Decode as Decode

      
        6
        7
         import Json.Encode as Encode

      
        7
        8
         

      
        8
        9
         

      ···
        27
        28
                 , body = Http.jsonBody body

      
        28
        29
                 , onResponse = options.onResponse

      
        29
        30
                 , decoder = Credentials.decode

      
        
        31
        +        }

      
        
        32
        +

      
        
        33
        +

      
        
        34
        +signup :

      
        
        35
        +    { onResponse : Result Http.Error () -> msg

      
        
        36
        +    , email : String

      
        
        37
        +    , password : String

      
        
        38
        +    }

      
        
        39
        +    -> Effect msg

      
        
        40
        +signup options =

      
        
        41
        +    let

      
        
        42
        +        body : Encode.Value

      
        
        43
        +        body =

      
        
        44
        +            Encode.object

      
        
        45
        +                [ ( "email", Encode.string options.email )

      
        
        46
        +                , ( "password", Encode.string options.password )

      
        
        47
        +                ]

      
        
        48
        +    in

      
        
        49
        +    Effect.sendApiRequest

      
        
        50
        +        { endpoint = "/api/v1/auth/signup"

      
        
        51
        +        , method = "POST"

      
        
        52
        +        , body = Http.jsonBody body

      
        
        53
        +        , onResponse = options.onResponse

      
        
        54
        +        , decoder = Decode.succeed ()

      
        30
        55
                 }

      
        31
        56
         

      
        32
        57
         

      
M web/src/Auth.elm
···
        27
        27
         

      
        28
        28
                 _ ->

      
        29
        29
                     Auth.Action.pushRoute

      
        30
        
        -                { path = Route.Path.SignIn

      
        
        30
        +                { path = Route.Path.Auth

      
        31
        31
                         , query = Dict.empty

      
        32
        32
                         , hash = Nothing

      
        33
        33
                         }

      
A web/src/Pages/Auth.elm
···
        
        1
        +module Pages.Auth exposing (Model, Msg, Variant, page)

      
        
        2
        +

      
        
        3
        +import Api

      
        
        4
        +import Api.Auth

      
        
        5
        +import Data.Credentials exposing (Credentials)

      
        
        6
        +import Effect exposing (Effect)

      
        
        7
        +import Html exposing (Html)

      
        
        8
        +import Html.Attributes as Attr

      
        
        9
        +import Html.Events

      
        
        10
        +import Http

      
        
        11
        +import Page exposing (Page)

      
        
        12
        +import Route exposing (Route)

      
        
        13
        +import Route.Path

      
        
        14
        +import Shared

      
        
        15
        +import View exposing (View)

      
        
        16
        +

      
        
        17
        +

      
        
        18
        +page : Shared.Model -> Route () -> Page Model Msg

      
        
        19
        +page shared _ =

      
        
        20
        +    Page.new

      
        
        21
        +        { init = init shared

      
        
        22
        +        , update = update

      
        
        23
        +        , subscriptions = subscriptions

      
        
        24
        +        , view = view

      
        
        25
        +        }

      
        
        26
        +

      
        
        27
        +

      
        
        28
        +

      
        
        29
        +-- INIT

      
        
        30
        +

      
        
        31
        +

      
        
        32
        +type alias Model =

      
        
        33
        +    { email : String

      
        
        34
        +    , password : String

      
        
        35
        +    , passwordAgain : String

      
        
        36
        +    , isSubmittingForm : Bool

      
        
        37
        +    , formVariant : Variant

      
        
        38
        +    , error : Maybe Http.Error

      
        
        39
        +    }

      
        
        40
        +

      
        
        41
        +

      
        
        42
        +init : Shared.Model -> () -> ( Model, Effect Msg )

      
        
        43
        +init shared _ =

      
        
        44
        +    ( { isSubmittingForm = False

      
        
        45
        +      , email = ""

      
        
        46
        +      , password = ""

      
        
        47
        +      , passwordAgain = ""

      
        
        48
        +      , formVariant = SignIn

      
        
        49
        +      , error = Nothing

      
        
        50
        +      }

      
        
        51
        +    , case shared.credentials of

      
        
        52
        +        Just _ ->

      
        
        53
        +            Effect.pushRoutePath Route.Path.Home_

      
        
        54
        +

      
        
        55
        +        Nothing ->

      
        
        56
        +            Effect.none

      
        
        57
        +    )

      
        
        58
        +

      
        
        59
        +

      
        
        60
        +

      
        
        61
        +-- UPDATE

      
        
        62
        +

      
        
        63
        +

      
        
        64
        +type Msg

      
        
        65
        +    = UserUpdatedInput Field String

      
        
        66
        +    | UserChangedFormVariant Variant

      
        
        67
        +    | UserClickedSubmit

      
        
        68
        +    | ApiSignInResponded (Result Http.Error Credentials)

      
        
        69
        +    | ApiSignUpResponded (Result Http.Error ())

      
        
        70
        +

      
        
        71
        +

      
        
        72
        +type Field

      
        
        73
        +    = Email

      
        
        74
        +    | Password

      
        
        75
        +    | PasswordAgain

      
        
        76
        +

      
        
        77
        +

      
        
        78
        +type Variant

      
        
        79
        +    = SignIn

      
        
        80
        +    | SignUp

      
        
        81
        +

      
        
        82
        +

      
        
        83
        +update : Msg -> Model -> ( Model, Effect Msg )

      
        
        84
        +update msg model =

      
        
        85
        +    case msg of

      
        
        86
        +        UserClickedSubmit ->

      
        
        87
        +            ( { model | isSubmittingForm = True }

      
        
        88
        +            , case model.formVariant of

      
        
        89
        +                SignIn ->

      
        
        90
        +                    Api.Auth.signin

      
        
        91
        +                        { onResponse = ApiSignInResponded

      
        
        92
        +                        , email = model.email

      
        
        93
        +                        , password = model.password

      
        
        94
        +                        }

      
        
        95
        +

      
        
        96
        +                SignUp ->

      
        
        97
        +                    Api.Auth.signup

      
        
        98
        +                        { onResponse = ApiSignUpResponded

      
        
        99
        +                        , email = model.email

      
        
        100
        +                        , password = model.password

      
        
        101
        +                        }

      
        
        102
        +            )

      
        
        103
        +

      
        
        104
        +        UserChangedFormVariant variant ->

      
        
        105
        +            ( { model | formVariant = variant }, Effect.none )

      
        
        106
        +

      
        
        107
        +        UserUpdatedInput Email email ->

      
        
        108
        +            ( { model | email = email }, Effect.none )

      
        
        109
        +

      
        
        110
        +        UserUpdatedInput Password password ->

      
        
        111
        +            ( { model | password = password }, Effect.none )

      
        
        112
        +

      
        
        113
        +        UserUpdatedInput PasswordAgain passwordAgain ->

      
        
        114
        +            ( { model | passwordAgain = passwordAgain }, Effect.none )

      
        
        115
        +

      
        
        116
        +        ApiSignInResponded (Ok credentials) ->

      
        
        117
        +            ( { model | isSubmittingForm = False }

      
        
        118
        +            , Effect.signin credentials

      
        
        119
        +            )

      
        
        120
        +

      
        
        121
        +        ApiSignInResponded (Err error) ->

      
        
        122
        +            ( { model | isSubmittingForm = False, error = Just error }, Effect.none )

      
        
        123
        +

      
        
        124
        +        ApiSignUpResponded (Ok ()) ->

      
        
        125
        +            -- TODO: show banner with that they have to activate account

      
        
        126
        +            ( { model | isSubmittingForm = False }, Effect.none )

      
        
        127
        +

      
        
        128
        +        ApiSignUpResponded (Err error) ->

      
        
        129
        +            ( { model | isSubmittingForm = False, error = Just error }, Effect.none )

      
        
        130
        +

      
        
        131
        +

      
        
        132
        +

      
        
        133
        +-- SUBSCRIPTIONS

      
        
        134
        +

      
        
        135
        +

      
        
        136
        +subscriptions : Model -> Sub Msg

      
        
        137
        +subscriptions _ =

      
        
        138
        +    Sub.none

      
        
        139
        +

      
        
        140
        +

      
        
        141
        +

      
        
        142
        +-- VIEW

      
        
        143
        +

      
        
        144
        +

      
        
        145
        +view : Model -> View Msg

      
        
        146
        +view model =

      
        
        147
        +    { title = "Authentication"

      
        
        148
        +    , body =

      
        
        149
        +        [ Html.div []

      
        
        150
        +            -- TODO: add oauth buttons

      
        
        151
        +            [ viewChangeVariant model.formVariant

      
        
        152
        +            , viewError model.error

      
        
        153
        +            , viewForm model

      
        
        154
        +            ]

      
        
        155
        +        ]

      
        
        156
        +    }

      
        
        157
        +

      
        
        158
        +

      
        
        159
        +viewChangeVariant : Variant -> Html Msg

      
        
        160
        +viewChangeVariant variant =

      
        
        161
        +    Html.div []

      
        
        162
        +        [ Html.button

      
        
        163
        +            [ Attr.disabled (variant == SignIn)

      
        
        164
        +            , Html.Events.onClick (UserChangedFormVariant SignIn)

      
        
        165
        +            ]

      
        
        166
        +            [ Html.text "Sign In" ]

      
        
        167
        +        , Html.button

      
        
        168
        +            [ Attr.disabled (variant == SignUp)

      
        
        169
        +            , Html.Events.onClick (UserChangedFormVariant SignUp)

      
        
        170
        +            ]

      
        
        171
        +            [ Html.text "Sign Up" ]

      
        
        172
        +        ]

      
        
        173
        +

      
        
        174
        +

      
        
        175
        +viewForm : Model -> Html Msg

      
        
        176
        +viewForm model =

      
        
        177
        +    Html.form [ Html.Events.onSubmit UserClickedSubmit ]

      
        
        178
        +        (case model.formVariant of

      
        
        179
        +            SignIn ->

      
        
        180
        +                [ viewFormInput { field = Email, value = model.email }

      
        
        181
        +                , viewFormInput { field = Password, value = model.password }

      
        
        182
        +                , viewFormControls model

      
        
        183
        +                ]

      
        
        184
        +

      
        
        185
        +            SignUp ->

      
        
        186
        +                [ viewFormInput { field = Email, value = model.email }

      
        
        187
        +                , viewFormInput { field = Password, value = model.password }

      
        
        188
        +                , viewFormInput { field = PasswordAgain, value = model.passwordAgain }

      
        
        189
        +                , viewFormControls model

      
        
        190
        +                ]

      
        
        191
        +        )

      
        
        192
        +

      
        
        193
        +

      
        
        194
        +viewError : Maybe Http.Error -> Html Msg

      
        
        195
        +viewError maybeError =

      
        
        196
        +    case maybeError of

      
        
        197
        +        Just error ->

      
        
        198
        +            Html.div [ Attr.style "color" "red" ]

      
        
        199
        +                [ Html.text (Api.errorToFriendlyMessage error) ]

      
        
        200
        +

      
        
        201
        +        Nothing ->

      
        
        202
        +            Html.text ""

      
        
        203
        +

      
        
        204
        +

      
        
        205
        +viewFormInput : { field : Field, value : String } -> Html Msg

      
        
        206
        +viewFormInput opts =

      
        
        207
        +    Html.div []

      
        
        208
        +        [ Html.label [] [ Html.text (fromFieldToLabel opts.field) ]

      
        
        209
        +        , Html.div []

      
        
        210
        +            [ Html.input

      
        
        211
        +                [ Attr.type_ (fromFieldToInputType opts.field)

      
        
        212
        +                , Attr.value opts.value

      
        
        213
        +                , Html.Events.onInput (UserUpdatedInput opts.field)

      
        
        214
        +                ]

      
        
        215
        +                []

      
        
        216
        +            ]

      
        
        217
        +        ]

      
        
        218
        +

      
        
        219
        +

      
        
        220
        +viewFormControls : Model -> Html Msg

      
        
        221
        +viewFormControls model =

      
        
        222
        +    Html.div []

      
        
        223
        +        [ Html.button

      
        
        224
        +            [ Attr.disabled (isFormDisabled model) ]

      
        
        225
        +            (case model.formVariant of

      
        
        226
        +                SignIn ->

      
        
        227
        +                    [ Html.text "Sign In" ]

      
        
        228
        +

      
        
        229
        +                SignUp ->

      
        
        230
        +                    [ Html.text "Sign Up" ]

      
        
        231
        +            )

      
        
        232
        +        ]

      
        
        233
        +

      
        
        234
        +

      
        
        235
        +isFormDisabled : Model -> Bool

      
        
        236
        +isFormDisabled model =

      
        
        237
        +    case model.formVariant of

      
        
        238
        +        SignIn ->

      
        
        239
        +            model.isSubmittingForm

      
        
        240
        +                || String.isEmpty model.email

      
        
        241
        +                || String.isEmpty model.password

      
        
        242
        +

      
        
        243
        +        SignUp ->

      
        
        244
        +            model.isSubmittingForm

      
        
        245
        +                || String.isEmpty model.email

      
        
        246
        +                || String.isEmpty model.password

      
        
        247
        +                || String.isEmpty model.passwordAgain

      
        
        248
        +                || (model.password /= model.passwordAgain)

      
        
        249
        +

      
        
        250
        +

      
        
        251
        +fromFieldToLabel : Field -> String

      
        
        252
        +fromFieldToLabel field =

      
        
        253
        +    case field of

      
        
        254
        +        Email ->

      
        
        255
        +            "Email address"

      
        
        256
        +

      
        
        257
        +        Password ->

      
        
        258
        +            "Password"

      
        
        259
        +

      
        
        260
        +        PasswordAgain ->

      
        
        261
        +            "Password again"

      
        
        262
        +

      
        
        263
        +

      
        
        264
        +fromFieldToInputType : Field -> String

      
        
        265
        +fromFieldToInputType field =

      
        
        266
        +    case field of

      
        
        267
        +        Email ->

      
        
        268
        +            "email"

      
        
        269
        +

      
        
        270
        +        Password ->

      
        
        271
        +            "password"

      
        
        272
        +

      
        
        273
        +        PasswordAgain ->

      
        
        274
        +            "password"

      
D web/src/Pages/SignIn.elm
···
        1
        
        -module Pages.SignIn exposing (Model, Msg, page)

      
        2
        
        -

      
        3
        
        -import Api

      
        4
        
        -import Api.Auth

      
        5
        
        -import Data.Credentials exposing (Credentials)

      
        6
        
        -import Effect exposing (Effect)

      
        7
        
        -import Html exposing (Html)

      
        8
        
        -import Html.Attributes as Attr

      
        9
        
        -import Html.Events

      
        10
        
        -import Http

      
        11
        
        -import Page exposing (Page)

      
        12
        
        -import Route exposing (Route)

      
        13
        
        -import Route.Path

      
        14
        
        -import Shared

      
        15
        
        -import View exposing (View)

      
        16
        
        -

      
        17
        
        -

      
        18
        
        -page : Shared.Model -> Route () -> Page Model Msg

      
        19
        
        -page shared _ =

      
        20
        
        -    Page.new

      
        21
        
        -        { init = init shared

      
        22
        
        -        , update = update

      
        23
        
        -        , subscriptions = subscriptions

      
        24
        
        -        , view = view

      
        25
        
        -        }

      
        26
        
        -

      
        27
        
        -

      
        28
        
        -

      
        29
        
        --- INIT

      
        30
        
        -

      
        31
        
        -

      
        32
        
        -type alias Model =

      
        33
        
        -    { email : String

      
        34
        
        -    , password : String

      
        35
        
        -    , isSubmittingForm : Bool

      
        36
        
        -    , error : Maybe Http.Error

      
        37
        
        -    }

      
        38
        
        -

      
        39
        
        -

      
        40
        
        -init : Shared.Model -> () -> ( Model, Effect Msg )

      
        41
        
        -init shared _ =

      
        42
        
        -    ( { isSubmittingForm = False

      
        43
        
        -      , email = ""

      
        44
        
        -      , password = ""

      
        45
        
        -      , error = Nothing

      
        46
        
        -      }

      
        47
        
        -    , case shared.credentials of

      
        48
        
        -        Just _ ->

      
        49
        
        -            Effect.pushRoutePath Route.Path.Home_

      
        50
        
        -

      
        51
        
        -        Nothing ->

      
        52
        
        -            Effect.none

      
        53
        
        -    )

      
        54
        
        -

      
        55
        
        -

      
        56
        
        -

      
        57
        
        --- UPDATE

      
        58
        
        -

      
        59
        
        -

      
        60
        
        -type Msg

      
        61
        
        -    = UserUpdatedInput Field String

      
        62
        
        -    | UserClickedSubmit

      
        63
        
        -    | ApiSignInResponded (Result Http.Error Credentials)

      
        64
        
        -

      
        65
        
        -

      
        66
        
        -type Field

      
        67
        
        -    = Email

      
        68
        
        -    | Password

      
        69
        
        -

      
        70
        
        -

      
        71
        
        -update : Msg -> Model -> ( Model, Effect Msg )

      
        72
        
        -update msg model =

      
        73
        
        -    case msg of

      
        74
        
        -        UserClickedSubmit ->

      
        75
        
        -            ( { model | isSubmittingForm = True }

      
        76
        
        -            , Api.Auth.signin

      
        77
        
        -                { onResponse = ApiSignInResponded

      
        78
        
        -                , email = model.email

      
        79
        
        -                , password = model.password

      
        80
        
        -                }

      
        81
        
        -            )

      
        82
        
        -

      
        83
        
        -        UserUpdatedInput Email email ->

      
        84
        
        -            ( { model | email = email }, Effect.none )

      
        85
        
        -

      
        86
        
        -        UserUpdatedInput Password password ->

      
        87
        
        -            ( { model | password = password }, Effect.none )

      
        88
        
        -

      
        89
        
        -        ApiSignInResponded (Ok credentials) ->

      
        90
        
        -            ( { model | isSubmittingForm = False }

      
        91
        
        -            , Effect.signin credentials

      
        92
        
        -            )

      
        93
        
        -

      
        94
        
        -        ApiSignInResponded (Err error) ->

      
        95
        
        -            ( { model | isSubmittingForm = False, error = Just error }

      
        96
        
        -            , Effect.none

      
        97
        
        -            )

      
        98
        
        -

      
        99
        
        -

      
        100
        
        -

      
        101
        
        --- SUBSCRIPTIONS

      
        102
        
        -

      
        103
        
        -

      
        104
        
        -subscriptions : Model -> Sub Msg

      
        105
        
        -subscriptions _ =

      
        106
        
        -    Sub.none

      
        107
        
        -

      
        108
        
        -

      
        109
        
        -

      
        110
        
        --- VIEW

      
        111
        
        -

      
        112
        
        -

      
        113
        
        -view : Model -> View Msg

      
        114
        
        -view model =

      
        115
        
        -    { title = "Sign-in"

      
        116
        
        -    , body =

      
        117
        
        -        [ Html.div []

      
        118
        
        -            [ Html.div []

      
        119
        
        -                [ Html.div []

      
        120
        
        -                    [ Html.h1 [] [ Html.text "Sign in" ]

      
        121
        
        -                    , viewError model.error

      
        122
        
        -                    , viewForm model

      
        123
        
        -                    ]

      
        124
        
        -                ]

      
        125
        
        -            ]

      
        126
        
        -        ]

      
        127
        
        -    }

      
        128
        
        -

      
        129
        
        -

      
        130
        
        -viewForm : Model -> Html Msg

      
        131
        
        -viewForm model =

      
        132
        
        -    Html.form [ Html.Events.onSubmit UserClickedSubmit ]

      
        133
        
        -        [ viewFormInput { field = Email, value = model.email }

      
        134
        
        -        , viewFormInput { field = Password, value = model.password }

      
        135
        
        -        , viewFormControls model

      
        136
        
        -        ]

      
        137
        
        -

      
        138
        
        -

      
        139
        
        -viewError : Maybe Http.Error -> Html Msg

      
        140
        
        -viewError maybeError =

      
        141
        
        -    case maybeError of

      
        142
        
        -        Just error ->

      
        143
        
        -            Html.div [ Attr.style "color" "red" ]

      
        144
        
        -                [ Html.text (Api.errorToFriendlyMessage error) ]

      
        145
        
        -

      
        146
        
        -        Nothing ->

      
        147
        
        -            Html.text ""

      
        148
        
        -

      
        149
        
        -

      
        150
        
        -viewFormInput : { field : Field, value : String } -> Html Msg

      
        151
        
        -viewFormInput opts =

      
        152
        
        -    Html.div []

      
        153
        
        -        [ Html.label [] [ Html.text (fromFieldToLabel opts.field) ]

      
        154
        
        -        , Html.div []

      
        155
        
        -            [ Html.input

      
        156
        
        -                [ Attr.type_ (fromFieldToInputType opts.field)

      
        157
        
        -                , Attr.value opts.value

      
        158
        
        -                , Html.Events.onInput (UserUpdatedInput opts.field)

      
        159
        
        -                ]

      
        160
        
        -                []

      
        161
        
        -            ]

      
        162
        
        -        ]

      
        163
        
        -

      
        164
        
        -

      
        165
        
        -viewFormControls : Model -> Html Msg

      
        166
        
        -viewFormControls model =

      
        167
        
        -    Html.div []

      
        168
        
        -        [ Html.button

      
        169
        
        -            [ Attr.disabled model.isSubmittingForm ]

      
        170
        
        -            [ Html.text "Sign In" ]

      
        171
        
        -        ]

      
        172
        
        -

      
        173
        
        -

      
        174
        
        -fromFieldToLabel : Field -> String

      
        175
        
        -fromFieldToLabel field =

      
        176
        
        -    case field of

      
        177
        
        -        Email ->

      
        178
        
        -            "Email address"

      
        179
        
        -

      
        180
        
        -        Password ->

      
        181
        
        -            "Password"

      
        182
        
        -

      
        183
        
        -

      
        184
        
        -fromFieldToInputType : Field -> String

      
        185
        
        -fromFieldToInputType field =

      
        186
        
        -    case field of

      
        187
        
        -        Email ->

      
        188
        
        -            "email"

      
        189
        
        -

      
        190
        
        -        Password ->

      
        191
        
        -            "password"