all repos

onasty @ 8ffca5c

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,8 +1,9 @@

-module Api.Auth exposing (refreshToken, signin) +module Api.Auth exposing (refreshToken, signin, signup) import Data.Credentials as Credentials exposing (Credentials) import Effect exposing (Effect) import Http +import Json.Decode as Decode import Json.Encode as Encode

@@ -27,6 +28,30 @@ , method = "POST"

, body = Http.jsonBody body , onResponse = options.onResponse , decoder = Credentials.decode + } + + +signup : + { onResponse : Result Http.Error () -> msg + , email : String + , password : String + } + -> Effect msg +signup options = + let + body : Encode.Value + body = + Encode.object + [ ( "email", Encode.string options.email ) + , ( "password", Encode.string options.password ) + ] + in + Effect.sendApiRequest + { endpoint = "/api/v1/auth/signup" + , method = "POST" + , body = Http.jsonBody body + , onResponse = options.onResponse + , decoder = Decode.succeed () }
M web/src/Auth.elm

@@ -27,7 +27,7 @@ }

_ -> Auth.Action.pushRoute - { path = Route.Path.SignIn + { path = Route.Path.Auth , query = Dict.empty , hash = Nothing }
A web/src/Pages/Auth.elm

@@ -0,0 +1,274 @@

+module Pages.Auth exposing (Model, Msg, Variant, page) + +import Api +import Api.Auth +import Data.Credentials exposing (Credentials) +import Effect exposing (Effect) +import Html exposing (Html) +import Html.Attributes as Attr +import Html.Events +import Http +import Page exposing (Page) +import Route exposing (Route) +import Route.Path +import Shared +import View exposing (View) + + +page : Shared.Model -> Route () -> Page Model Msg +page shared _ = + Page.new + { init = init shared + , update = update + , subscriptions = subscriptions + , view = view + } + + + +-- INIT + + +type alias Model = + { email : String + , password : String + , passwordAgain : String + , isSubmittingForm : Bool + , formVariant : Variant + , error : Maybe Http.Error + } + + +init : Shared.Model -> () -> ( Model, Effect Msg ) +init shared _ = + ( { isSubmittingForm = False + , email = "" + , password = "" + , passwordAgain = "" + , formVariant = SignIn + , error = Nothing + } + , case shared.credentials of + Just _ -> + Effect.pushRoutePath Route.Path.Home_ + + Nothing -> + Effect.none + ) + + + +-- UPDATE + + +type Msg + = UserUpdatedInput Field String + | UserChangedFormVariant Variant + | UserClickedSubmit + | ApiSignInResponded (Result Http.Error Credentials) + | ApiSignUpResponded (Result Http.Error ()) + + +type Field + = Email + | Password + | PasswordAgain + + +type Variant + = SignIn + | SignUp + + +update : Msg -> Model -> ( Model, Effect Msg ) +update msg model = + case msg of + UserClickedSubmit -> + ( { model | isSubmittingForm = True } + , case model.formVariant of + SignIn -> + Api.Auth.signin + { onResponse = ApiSignInResponded + , email = model.email + , password = model.password + } + + SignUp -> + Api.Auth.signup + { onResponse = ApiSignUpResponded + , email = model.email + , password = model.password + } + ) + + UserChangedFormVariant variant -> + ( { model | formVariant = variant }, Effect.none ) + + UserUpdatedInput Email email -> + ( { model | email = email }, Effect.none ) + + UserUpdatedInput Password password -> + ( { model | password = password }, Effect.none ) + + UserUpdatedInput PasswordAgain passwordAgain -> + ( { model | passwordAgain = passwordAgain }, Effect.none ) + + ApiSignInResponded (Ok credentials) -> + ( { model | isSubmittingForm = False } + , Effect.signin credentials + ) + + ApiSignInResponded (Err error) -> + ( { model | isSubmittingForm = False, error = Just error }, Effect.none ) + + ApiSignUpResponded (Ok ()) -> + -- TODO: show banner with that they have to activate account + ( { model | isSubmittingForm = False }, Effect.none ) + + ApiSignUpResponded (Err error) -> + ( { model | isSubmittingForm = False, error = Just error }, Effect.none ) + + + +-- SUBSCRIPTIONS + + +subscriptions : Model -> Sub Msg +subscriptions _ = + Sub.none + + + +-- VIEW + + +view : Model -> View Msg +view model = + { title = "Authentication" + , body = + [ Html.div [] + -- TODO: add oauth buttons + [ viewChangeVariant model.formVariant + , viewError model.error + , viewForm model + ] + ] + } + + +viewChangeVariant : Variant -> Html Msg +viewChangeVariant variant = + Html.div [] + [ Html.button + [ Attr.disabled (variant == SignIn) + , Html.Events.onClick (UserChangedFormVariant SignIn) + ] + [ Html.text "Sign In" ] + , Html.button + [ Attr.disabled (variant == SignUp) + , Html.Events.onClick (UserChangedFormVariant SignUp) + ] + [ Html.text "Sign Up" ] + ] + + +viewForm : Model -> Html Msg +viewForm model = + Html.form [ Html.Events.onSubmit UserClickedSubmit ] + (case model.formVariant of + SignIn -> + [ viewFormInput { field = Email, value = model.email } + , viewFormInput { field = Password, value = model.password } + , viewFormControls model + ] + + SignUp -> + [ viewFormInput { field = Email, value = model.email } + , viewFormInput { field = Password, value = model.password } + , viewFormInput { field = PasswordAgain, value = model.passwordAgain } + , viewFormControls model + ] + ) + + +viewError : Maybe Http.Error -> Html Msg +viewError maybeError = + case maybeError of + Just error -> + Html.div [ Attr.style "color" "red" ] + [ Html.text (Api.errorToFriendlyMessage error) ] + + Nothing -> + Html.text "" + + +viewFormInput : { field : Field, value : String } -> Html Msg +viewFormInput opts = + Html.div [] + [ Html.label [] [ Html.text (fromFieldToLabel opts.field) ] + , Html.div [] + [ Html.input + [ Attr.type_ (fromFieldToInputType opts.field) + , Attr.value opts.value + , Html.Events.onInput (UserUpdatedInput opts.field) + ] + [] + ] + ] + + +viewFormControls : Model -> Html Msg +viewFormControls model = + Html.div [] + [ Html.button + [ Attr.disabled (isFormDisabled model) ] + (case model.formVariant of + SignIn -> + [ Html.text "Sign In" ] + + SignUp -> + [ Html.text "Sign Up" ] + ) + ] + + +isFormDisabled : Model -> Bool +isFormDisabled model = + case model.formVariant of + SignIn -> + model.isSubmittingForm + || String.isEmpty model.email + || String.isEmpty model.password + + SignUp -> + model.isSubmittingForm + || String.isEmpty model.email + || String.isEmpty model.password + || String.isEmpty model.passwordAgain + || (model.password /= model.passwordAgain) + + +fromFieldToLabel : Field -> String +fromFieldToLabel field = + case field of + Email -> + "Email address" + + Password -> + "Password" + + PasswordAgain -> + "Password again" + + +fromFieldToInputType : Field -> String +fromFieldToInputType field = + case field of + Email -> + "email" + + Password -> + "password" + + PasswordAgain -> + "password"
D

@@ -1,191 +0,0 @@

-module Pages.SignIn exposing (Model, Msg, page) - -import Api -import Api.Auth -import Data.Credentials exposing (Credentials) -import Effect exposing (Effect) -import Html exposing (Html) -import Html.Attributes as Attr -import Html.Events -import Http -import Page exposing (Page) -import Route exposing (Route) -import Route.Path -import Shared -import View exposing (View) - - -page : Shared.Model -> Route () -> Page Model Msg -page shared _ = - Page.new - { init = init shared - , update = update - , subscriptions = subscriptions - , view = view - } - - - --- INIT - - -type alias Model = - { email : String - , password : String - , isSubmittingForm : Bool - , error : Maybe Http.Error - } - - -init : Shared.Model -> () -> ( Model, Effect Msg ) -init shared _ = - ( { isSubmittingForm = False - , email = "" - , password = "" - , error = Nothing - } - , case shared.credentials of - Just _ -> - Effect.pushRoutePath Route.Path.Home_ - - Nothing -> - Effect.none - ) - - - --- UPDATE - - -type Msg - = UserUpdatedInput Field String - | UserClickedSubmit - | ApiSignInResponded (Result Http.Error Credentials) - - -type Field - = Email - | Password - - -update : Msg -> Model -> ( Model, Effect Msg ) -update msg model = - case msg of - UserClickedSubmit -> - ( { model | isSubmittingForm = True } - , Api.Auth.signin - { onResponse = ApiSignInResponded - , email = model.email - , password = model.password - } - ) - - UserUpdatedInput Email email -> - ( { model | email = email }, Effect.none ) - - UserUpdatedInput Password password -> - ( { model | password = password }, Effect.none ) - - ApiSignInResponded (Ok credentials) -> - ( { model | isSubmittingForm = False } - , Effect.signin credentials - ) - - ApiSignInResponded (Err error) -> - ( { model | isSubmittingForm = False, error = Just error } - , Effect.none - ) - - - --- SUBSCRIPTIONS - - -subscriptions : Model -> Sub Msg -subscriptions _ = - Sub.none - - - --- VIEW - - -view : Model -> View Msg -view model = - { title = "Sign-in" - , body = - [ Html.div [] - [ Html.div [] - [ Html.div [] - [ Html.h1 [] [ Html.text "Sign in" ] - , viewError model.error - , viewForm model - ] - ] - ] - ] - } - - -viewForm : Model -> Html Msg -viewForm model = - Html.form [ Html.Events.onSubmit UserClickedSubmit ] - [ viewFormInput { field = Email, value = model.email } - , viewFormInput { field = Password, value = model.password } - , viewFormControls model - ] - - -viewError : Maybe Http.Error -> Html Msg -viewError maybeError = - case maybeError of - Just error -> - Html.div [ Attr.style "color" "red" ] - [ Html.text (Api.errorToFriendlyMessage error) ] - - Nothing -> - Html.text "" - - -viewFormInput : { field : Field, value : String } -> Html Msg -viewFormInput opts = - Html.div [] - [ Html.label [] [ Html.text (fromFieldToLabel opts.field) ] - , Html.div [] - [ Html.input - [ Attr.type_ (fromFieldToInputType opts.field) - , Attr.value opts.value - , Html.Events.onInput (UserUpdatedInput opts.field) - ] - [] - ] - ] - - -viewFormControls : Model -> Html Msg -viewFormControls model = - Html.div [] - [ Html.button - [ Attr.disabled model.isSubmittingForm ] - [ Html.text "Sign In" ] - ] - - -fromFieldToLabel : Field -> String -fromFieldToLabel field = - case field of - Email -> - "Email address" - - Password -> - "Password" - - -fromFieldToInputType : Field -> String -fromFieldToInputType field = - case field of - Email -> - "email" - - Password -> - "password"