4 files changed,
301 insertions(+),
193 deletions(-)
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
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"