mail based rss feed aggregator
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

redo basically the whole login flow with passwords instead its much simpler

ollie 1eca98c8 109fafb1

+101 -222
+101 -222
src/eater/ui/main_ui.gleam
··· 52 52 } 53 53 54 54 fn init(data: ModelData) -> #(Model, Effect(Message)) { 55 - #(EmailForm(data, email_form()), effect.none()) 55 + #(LoginModel(data, login_form()), effect.none()) 56 56 } 57 57 58 58 // model ------------------------------------------------------------------------ ··· 70 70 71 71 fn update_data(data: ModelData, model: Model) -> Model { 72 72 case model { 73 - EmailForm(..) -> EmailForm(..model, data:) 74 - EmailSending(..) -> EmailSending(..model, data:) 75 - PasswordForm(..) -> PasswordForm(..model, data:) 73 + LoginModel(..) -> LoginModel(..model, data:) 76 74 LoggedIn(..) -> LoggedIn(..model, data:) 77 75 } 78 76 } ··· 99 97 ) 100 98 } 101 99 100 + type FormUser { 101 + FormUser(email: String, password: String) 102 + } 103 + 102 104 /// 103 105 pub opaque type Model { 104 - EmailForm(data: ModelData, form: Form(Login)) 105 - EmailSending(data: ModelData, user: user.User) 106 - PasswordForm( 107 - data: ModelData, 108 - form: Form(String), 109 - password_should_be: String, 110 - user: user.User, 111 - ) 106 + LoginModel(data: ModelData, form: form.Form(FormUser)) 112 107 LoggedIn(data: ModelData, user: user.User) 113 108 } 114 109 ··· 116 111 /// 117 112 fn describe_model(model: Model) { 118 113 case model { 119 - EmailForm(data: _, form:) -> [ 114 + LoginModel(data: _, form:) -> [ 120 115 woof.field("model", "EmailForm"), 121 116 woof.field("form-content", form.field_value(form, "email")), 122 117 ] 123 - EmailSending(data: _, user:) -> [ 124 - woof.field("model", "EmailSending"), 125 - woof.field("to", user.email), 126 - ] 127 - PasswordForm(data: _, form:, password_should_be:, user:) -> [ 128 - woof.field("model", "PasswordForm"), 129 - woof.field("form-content", form.field_value(form, "password")), 130 - woof.field("password-should-be", password_should_be), 131 - woof.field("email", user.email), 132 - ] 133 118 LoggedIn(data: _, user:) -> [ 134 119 woof.field("model", "LoggedIn"), 135 120 woof.field("user-email", user.email), ··· 138 123 } 139 124 140 125 pub opaque type Message { 141 - UserSubmittedEmail(result: Result(Login, Form(Login))) 126 + UserSubmittedLoginForm(result: Result(FormUser, Form(FormUser))) 127 + ServerVerifiedLogin(valid: Bool, user: user.User) 128 + DatabaseReturnedUser( 129 + in_db: Result(Result(user.User, String), sqlight.Error), 130 + from_form: FormUser, 131 + ) 132 + TimerDismissedToast 133 + 134 + // old 142 135 /// 143 136 ///```gleam 144 137 ///Result(Result(user.User, email), db_error) 145 138 ///``` 146 - DatabaseReturnedUser(Result(Result(user.User, String), sqlight.Error)) 139 + ServerCreatedNewUser(user: user.User) 147 140 DatabaseAddedNewUser(Result(user.User, sqlight.Error)) 148 141 UserSubmittedPassword(result: Result(String, Form(String))) 149 142 150 143 ServerGeneratedPassword(password: String) 151 144 ServerSentPassword(Result(String, gsmtp.Error)) 152 - TimerDismissedToast 153 145 } 154 146 155 147 /// describe a message using structured data 156 148 /// 157 149 fn describe_message(message: Message) { 158 150 case message { 159 - UserSubmittedEmail(Ok(login)) -> [ 151 + _ -> [#("empty", "ill do em later")] 152 + UserSubmittedLoginForm(Ok(login)) -> [ 160 153 woof.field("message", "UserSubmittedEmail"), 161 154 woof.field("status", "ok"), 162 155 woof.field("email", login.email), 163 156 ] 164 - UserSubmittedEmail(Error(_)) -> [ 157 + UserSubmittedLoginForm(Error(_)) -> [ 165 158 woof.field("message", "UserSubmittedEmail"), 166 159 woof.field("status", "error"), 167 160 ] 168 - DatabaseReturnedUser(Ok(Ok(_))) -> [ 161 + DatabaseReturnedUser(Ok(Ok(_)), _) -> [ 169 162 woof.field("message", "DatabaseReturnedUser"), 170 163 woof.field("status", "ok"), 171 164 woof.field("details", "found"), 172 165 ] 173 - DatabaseReturnedUser(Ok(Error(_))) -> [ 166 + DatabaseReturnedUser(Ok(Error(_)), _) -> [ 174 167 woof.field("message", "DatabaseReturnedUser"), 175 168 woof.field("status", "ok"), 176 169 woof.field("status", "not-found"), 177 170 ] 178 - DatabaseReturnedUser(Error(_)) -> [ 171 + DatabaseReturnedUser(Error(_), _) -> [ 179 172 woof.field("message", "DatabaseReturnedUser"), 180 173 woof.field("status", "error"), 174 + ] 175 + ServerCreatedNewUser(user) -> [ 176 + woof.field("message", "ServerCreatedNewUser"), 177 + woof.field("email", user.email), 178 + woof.field("id", user.id |> uuid.to_string()), 181 179 ] 182 180 DatabaseAddedNewUser(Ok(_)) -> [ 183 181 woof.field("message", "DatabaseAddedNewUser"), ··· 222 220 False -> Nil 223 221 } 224 222 225 - case message, model { 226 - // login - enter email ------------------------------------------------------ 227 - UserSubmittedEmail(Ok(login)), EmailForm(..) -> #( 223 + case message { 224 + UserSubmittedLoginForm(Error(form)) -> #( 225 + LoginModel(model.data, form), 226 + effect.none(), 227 + ) 228 + UserSubmittedLoginForm(Ok(login)) -> #( 228 229 model, 229 230 fetch_user_from_database(model.data, login), 230 231 ) 231 - UserSubmittedEmail(Error(form)), _ -> #( 232 - EmailForm(model.data, form), 233 - effect.none(), 234 - ) 235 - // should never happen 236 - UserSubmittedEmail(_), _ -> #(model, effect.none()) 237 - 238 - DatabaseReturnedUser(Ok(Ok(user))), _ -> #( 239 - EmailSending(model.data, user), 240 - generate_one_time_password(), 232 + DatabaseReturnedUser(Ok(Ok(user)), from_form) -> #( 233 + model, 234 + verify_login(user, from_form), 241 235 ) 242 - 243 - DatabaseReturnedUser(Ok(Error(email))), _ -> { 244 - // signups are enabled, so we make a new user 245 - use <- bool.guard(model.data.allow_signups, return: #( 246 - model, 247 - make_user_from_email(model.data, email), 248 - )) 249 - 250 - // signups arnt enabled, tell the user 236 + DatabaseReturnedUser(Ok(Error(_)), _) -> { 237 + case model.data.allow_signups { 238 + True -> { 239 + let toast = 240 + toaster.Toast( 241 + title: Some("Invalid login"), 242 + message: "Did you mean to create a new account?\nYou should go to the signup page in that case :3", 243 + options: toast.default_options(toast.Info), 244 + ) 245 + do_toast(toast, model) 246 + } 247 + False -> { 248 + let toast = 249 + toaster.Toast( 250 + title: Some("Invalid login"), 251 + message: "Try again or contact " 252 + <> smtp.sender_email(model.data.smtp_environment) 253 + <> " about getting / recovering an account.", 254 + options: toast.default_options(toast.Info), 255 + ) 256 + do_toast(toast, model) 257 + } 258 + } 259 + } 260 + DatabaseReturnedUser(Error(_), _) -> { 251 261 let toast = 252 262 toaster.Toast( 253 - title: Some("Invalid email address"), 254 - message: "This email doesn't exist and signups are disabled", 255 - options: toast.default_options(toast.Warning), 263 + title: None, 264 + message: "I ran into a problem", 265 + options: toast.default_options(toast.Danger), 256 266 ) 257 267 do_toast(toast, model) 258 268 } 259 - DatabaseReturnedUser(Error(db_error)), _ -> { 260 - model.data.logger 261 - |> woof.log(woof.Error, "Fetching user from database failed", [ 262 - woof.field("details", string.inspect(db_error)), 263 - ]) 264 - #(model, effect.none()) 265 - } 266 - 267 - // new user was created, lets generate a one time password for them 268 - // so they may log in 269 - DatabaseAddedNewUser(Ok(user)), _ -> #( 270 - EmailSending(model.data, user), 271 - generate_one_time_password(), 269 + ServerVerifiedLogin(valid: True, user:) -> #( 270 + LoggedIn(model.data, user), 271 + effect.none(), 272 272 ) 273 - 274 - // creating a new user failed 275 - DatabaseAddedNewUser(Error(db_error)), _ -> { 276 - model.data.logger 277 - |> woof.log(woof.Error, "Adding user to database failed", [ 278 - woof.field("details", string.inspect(db_error)), 279 - ]) 280 - #(model, effect.none()) 281 - } 282 - 283 - // login - generate password ------------------------------------------------ 284 - ServerGeneratedPassword(password), EmailSending(..) -> { 285 - #( 286 - model, 287 - send_one_time_password( 288 - password, 289 - model.user, 290 - model.data.smtp_environment, 291 - ), 292 - ) 293 - } 294 - // should never happen 295 - ServerGeneratedPassword(_), _ -> #(model, effect.none()) 296 - 297 - // login - send password ---------------------------------------------------- 298 - ServerSentPassword(Ok(password)), EmailSending(data: _, user:) -> { 273 + ServerVerifiedLogin(valid: False, user: _) -> { 299 274 let toast = 300 275 toaster.Toast( 301 - title: None, 302 - message: "One time password was sent", 276 + title: Some("Invalid login"), 277 + message: "Try again, i guess", 303 278 options: toast.default_options(toast.Info), 304 279 ) 305 - 306 - let #(model, effect) = do_toast(toast, model) 307 - let model = 308 - PasswordForm( 309 - form: one_time_password_form(), 310 - password_should_be: password, 311 - user:, 312 - data: model.data, 313 - ) 314 - 315 - #(model, effect) 280 + do_toast(toast, model) 316 281 } 317 282 318 - ServerSentPassword(Error(_)), EmailSending(user:, ..) -> { 319 - let toast = 320 - toaster.Toast( 321 - title: Some("Error"), 322 - message: "Failed to send one time password. Try again later or contact " 323 - <> smtp.sender_email(model.data.smtp_environment) 324 - <> " with " 325 - <> model.data.csrf_token 326 - <> " if the problem persists.", 327 - options: toast.default_options(toast.Warning), 328 - ) 329 - 330 - let #(model, effect) = do_toast(toast, model) 331 - 332 - #(EmailForm(model.data, email_form_with_data(user.email)), effect) 333 - } 334 - // should never happen 335 - ServerSentPassword(_), _ -> #(model, effect.none()) 336 - 337 - // login - password --------------------------------------------------------- 338 - UserSubmittedPassword(Error(form)), PasswordForm(..) as model -> #( 339 - PasswordForm(..model, form:), 340 - effect.none(), 341 - ) 342 - UserSubmittedPassword(Ok(user_password)), 343 - PasswordForm(password_should_be: correct_password, user:, ..) 344 - -> { 345 - case user_password == correct_password { 346 - True -> #(LoggedIn(model.data, user), effect.none()) 347 - False -> 348 - do_toast( 349 - toaster.Toast( 350 - title: Some("Incorrect"), 351 - message: "The password you entered was incorrect", 352 - options: toast.default_options(toast.Warning), 353 - ), 354 - model, 355 - ) 356 - } 357 - } 358 - // should never happen 359 - UserSubmittedPassword(_), _ -> #(model, effect.none()) 360 - 361 - // user ui ------------------------------------------------------------------ 362 - // toasts ------------------------------------------------------------------- 363 - TimerDismissedToast, _ -> { 283 + TimerDismissedToast -> { 364 284 // the amount of toasts should stay small enough that this isnt an issue 365 285 let toasts = 366 286 model.data.toasts ··· 371 291 372 292 #(model, effect.none()) 373 293 } 294 + 295 + _ -> todo 374 296 } 375 297 } 376 298 377 - /// Create and add a new user to the database 378 - /// 379 - /// `DatabaseAddedNewUser` 380 - /// 381 - fn make_user_from_email(data: ModelData, email: String) -> Effect(Message) { 299 + fn verify_login(user: user.User, from_form: FormUser) -> Effect(Message) { 382 300 use dispatch <- effect.from 383 301 384 - let user = user.new(email) 302 + let form_password = user.hash_password(from_form.password) 385 303 386 - database.add_user(user:, into: data.database) 387 - |> result.replace(user) 388 - |> DatabaseAddedNewUser 304 + { form_password == user.password_hash && from_form.email == user.email } 305 + |> ServerVerifiedLogin(user) 389 306 |> dispatch 390 307 } 391 308 ··· 393 310 /// 394 311 /// `DatabaseReturnedUser` 395 312 /// 396 - fn fetch_user_from_database(data: ModelData, login: Login) -> Effect(Message) { 313 + fn fetch_user_from_database(data: ModelData, user: FormUser) -> Effect(Message) { 397 314 use dispatch <- effect.from 398 315 399 - database.user_by_email(data.database, login.email) 400 - |> DatabaseReturnedUser 316 + database.user_by_email(data.database, user.email) 317 + |> DatabaseReturnedUser(user) 401 318 |> dispatch 402 319 } 403 320 ··· 452 369 fn view(model: Model) -> Element(Message) { 453 370 html.main([], [ 454 371 case model { 455 - EmailForm(form:, ..) -> view_email_form(form:, busy: False) 456 - EmailSending(user:, ..) -> 457 - view_email_form(email_form_with_data(user.email), busy: True) 458 - // TODO: empty form cause apparently it doesnt ?! 459 - PasswordForm(form:, ..) -> 460 - view_confirm_one_time_password(form:, busy: False) 372 + LoginModel(data: _, form:) -> view_login_form(form:, busy: False) 461 373 LoggedIn(data:, user:) -> view_logged_in(data, user) 462 374 }, 463 375 // button.button([event.on_click(SpawnToast)], [element.text("spawn toast")]), ··· 477 389 ]) 478 390 } 479 391 480 - fn view_email_form(form form: Form(Login), busy busy: Bool) -> Element(Message) { 392 + fn view_login_form( 393 + form form: Form(FormUser), 394 + busy busy: Bool, 395 + ) -> Element(Message) { 481 396 let submitted = fn(fields) { 482 397 form 483 398 |> form.add_values(fields) 484 399 |> form.run 485 - |> UserSubmittedEmail 400 + |> UserSubmittedLoginForm 486 401 } 487 402 488 403 // TODO: fix minified oat missing ··· 507 422 ], 508 423 [ 509 424 field_input(form, name: "email", kind: "text", label: "Email"), 425 + field_input( 426 + form, 427 + name: "password", 428 + kind: "password", 429 + label: "Password", 430 + ), 510 431 html.div([], [html.input([attribute.type_("submit")])]), 511 432 ], 512 433 ), ··· 516 437 ) 517 438 } 518 439 519 - fn view_confirm_one_time_password( 520 - form form: Form(String), 521 - busy busy: Bool, 522 - ) -> Element(Message) { 523 - let submitted = fn(fields) { 524 - form 525 - |> form.add_values(fields) 526 - |> form.run 527 - |> UserSubmittedPassword 528 - } 529 - 530 - // TODO: clear password field on first load (apparently it thinks the email and password field input are the same) 531 - html.div( 532 - [ 533 - attribute.class("container vstack"), 534 - attribute.aria_busy(busy), 535 - attribute.attribute("data-spinner", "large overlay"), 536 - ], 537 - [ 538 - html.div([attribute.class("row")], [ 539 - card.card([attribute.class("col-4 offset-4")], [ 540 - card.header([], [html.h3([], [element.text("Password")])]), 541 - gform.form([attribute.method("POST"), event.on_submit(submitted)], [ 542 - field_input( 543 - form, 544 - name: "password", 545 - kind: "text", 546 - label: "One time password", 547 - ), 548 - html.div([], [html.input([attribute.type_("submit")])]), 549 - ]), 550 - ]), 551 - ]), 552 - ], 553 - ) 554 - } 555 - 556 440 // formal ----------------------------------------------------------------------- 557 441 558 - pub opaque type Login { 559 - Login(email: String) 560 - } 561 - 562 - fn email_form_with_data(email: String) -> Form(Login) { 563 - email_form() 564 - |> form.add_string("email", email) 565 - } 566 - 567 - fn email_form() -> Form(Login) { 442 + fn login_form() -> Form(FormUser) { 568 443 form.new({ 569 444 use email <- form.field("email", { form.parse_email }) 445 + use password <- form.field("password", { 446 + form.parse_string 447 + |> form.check_string_length_more_than(8) 448 + }) 570 449 571 - form.success(Login(email: email)) 450 + form.success(FormUser(email:, password:)) 572 451 }) 573 452 } 574 453