mail based rss feed aggregator
2
fork

Configure Feed

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

signup process

ollie 12b34ff5 c1b5b9de

+429 -56
+14 -5
README.md
··· 40 40 41 41 ### TODO (step 2) 42 42 - [ ] lustre ui (maybe) server component 43 - - [ ] login 44 - - [x] email one time password 45 - - [x] login flow email -> password -> login 46 - - [ ] (maybe) password 47 - - [ ] sessions 43 + - [x] login 44 + - [x] password login 45 + - [ ] tests 46 + - [x] signups 47 + - [x] signup flow 48 + - [x] enter email + password 49 + - [x] one time password via email 50 + - [x] confirm one time password 51 + - [x] logged in 52 + - [ ] tests 53 + - [ ] sessions? 48 54 - [ ] configure rss feeds to track per user 49 55 - [ ] create / delete fetchers and sender accordingly 50 56 - [ ] update feed subscriptions in database ··· 85 91 the username to authenticate with the smtp server ex: rss@ollie.earth 86 92 - `SMTP_PASSWORD` 87 93 the password to use when authenticating with the smtp server 94 + 95 + - `ADMIN_PASSWORD` 96 + the password to use for the default(`SMTP_SENDER_EMAIL`) admin 88 97 89 98 --- 90 99
+414 -50
src/eater/ui/main_ui.gleam
··· 16 16 // This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. [cite: 5] 17 17 // See the Licence for the specific language governing permissions and limitations. [cite: 6] 18 18 19 + import eater/configuration 19 20 import eater/database 20 21 import eater/smtp 21 22 import eater/ui/toaster ··· 26 27 import glaze/oat/form as gform 27 28 import glaze/oat/toast 28 29 import gleam/bit_array 29 - import gleam/bool 30 30 import gleam/crypto 31 31 import gleam/erlang/process 32 + import gleam/int 32 33 import gleam/list 33 34 import gleam/option.{None, Some} 34 - import gleam/result 35 35 import gleam/string 36 36 import lustre 37 37 import lustre/attribute ··· 52 52 } 53 53 54 54 fn init(data: ModelData) -> #(Model, Effect(Message)) { 55 - #(LoginModel(data, login_form()), effect.none()) 55 + #(LoginForm(data, login_form()), effect.none()) 56 56 } 57 57 58 58 // model ------------------------------------------------------------------------ ··· 62 62 csrf_token: String, 63 63 database: sqlight.Connection, 64 64 smtp_environment: smtp.SmtpEnvironment, 65 - allow_signups: Bool, 65 + configuration: configuration.AppConfig, 66 66 toasts: List(toaster.Toast), 67 67 logger: woof.Logger, 68 68 ) ··· 70 70 71 71 fn update_data(data: ModelData, model: Model) -> Model { 72 72 case model { 73 - LoginModel(..) -> LoginModel(..model, data:) 73 + SignUpForm(..) -> SignUpForm(..model, data:) 74 + ConfirmOneTimePassword(..) -> ConfirmOneTimePassword(..model, data:) 75 + LoginForm(..) -> LoginForm(..model, data:) 74 76 LoggedIn(..) -> LoggedIn(..model, data:) 75 77 } 76 78 } ··· 81 83 csrf_token _csrf_token: String, 82 84 database database: sqlight.Connection, 83 85 smtp_environment smtp_environment: smtp.SmtpEnvironment, 84 - allow_signups allow_signups: Bool, 86 + configuration configuration: configuration.AppConfig, 85 87 ) -> ModelData { 86 88 // for some amount of per-session traceability; 87 89 // this should really be replaced with the csrf token, once that is properly implemented ··· 91 93 csrf_token: session_id, 92 94 database:, 93 95 smtp_environment:, 94 - allow_signups:, 96 + configuration:, 95 97 toasts: [], 96 98 logger: woof.new("UI-" <> session_id), 97 99 ) ··· 103 105 104 106 /// 105 107 pub opaque type Model { 106 - LoginModel(data: ModelData, form: form.Form(FormUser)) 108 + SignUpForm(data: ModelData, form: form.Form(FormUser)) 109 + ConfirmOneTimePassword( 110 + data: ModelData, 111 + user: FormUser, 112 + form: form.Form(String), 113 + password_to_confirm: String, 114 + attempts_left: Int, 115 + ) 116 + LoginForm(data: ModelData, form: form.Form(FormUser)) 107 117 LoggedIn(data: ModelData, user: user.User) 108 118 } 109 119 ··· 111 121 /// 112 122 fn describe_model(model: Model) { 113 123 case model { 114 - LoginModel(data: _, form:) -> [ 115 - woof.field("model", "EmailForm"), 116 - woof.field("form-content", form.field_value(form, "email")), 124 + SignUpForm(_data, form) -> [ 125 + woof.field("model", "SignUpForm"), 126 + woof.field("form-email", form.field_value(form, "email")), 127 + ] 128 + ConfirmOneTimePassword( 129 + data: _, 130 + user:, 131 + form: _, 132 + password_to_confirm: _, 133 + attempts_left:, 134 + ) -> [ 135 + woof.field("model", "SignUpFormConfirmOtp"), 136 + woof.field("form-user-email", user.email), 137 + woof.int_field("attempts_left", attempts_left), 138 + ] 139 + LoginForm(data: _, form:) -> [ 140 + woof.field("model", "LoginForm"), 141 + woof.field("form-email", form.field_value(form, "email")), 117 142 ] 118 143 LoggedIn(data: _, user:) -> [ 119 144 woof.field("model", "LoggedIn"), ··· 123 148 } 124 149 125 150 pub opaque type Message { 151 + // login 126 152 UserSubmittedLoginForm(result: Result(FormUser, Form(FormUser))) 127 153 ServerVerifiedLogin(valid: Bool, user: user.User) 128 154 DatabaseReturnedUser( 129 155 in_db: Result(Result(user.User, String), sqlight.Error), 130 156 from_form: FormUser, 131 157 ) 132 - TimerDismissedToast 133 158 159 + // signup 160 + UserClickedGotoSignUp 161 + UserSubmittedSignUpForm(Result(FormUser, Form(FormUser))) 162 + ServerGeneratedPassword(password: String, user: FormUser) 163 + ServerSentPassword( 164 + Result(Nil, gsmtp.Error), 165 + password_to_confirm: String, 166 + user: FormUser, 167 + ) 168 + UserSubmittedOneTimePassword(result: Result(String, Form(String))) 169 + 170 + // toast 171 + TimerDismissedToast 134 172 // old 135 173 /// 136 174 ///```gleam 137 175 ///Result(Result(user.User, email), db_error) 138 176 ///``` 139 177 ServerCreatedNewUser(user: user.User) 140 - DatabaseAddedNewUser(Result(user.User, sqlight.Error)) 141 - UserSubmittedPassword(result: Result(String, Form(String))) 142 - 143 - ServerGeneratedPassword(password: String) 144 - ServerSentPassword(Result(String, gsmtp.Error)) 178 + DatabasePersistedNewUser(Result(user.User, sqlight.Error)) 179 + UserClickedGotoLogIn 145 180 } 146 181 147 182 /// describe a message using structured data 148 183 /// 149 184 fn describe_message(message: Message) { 150 185 case message { 151 - _ -> [#("empty", "ill do em later")] 152 186 UserSubmittedLoginForm(Ok(login)) -> [ 153 187 woof.field("message", "UserSubmittedEmail"), 154 188 woof.field("status", "ok"), ··· 177 211 woof.field("email", user.email), 178 212 woof.field("id", user.id |> uuid.to_string()), 179 213 ] 180 - DatabaseAddedNewUser(Ok(_)) -> [ 214 + DatabasePersistedNewUser(Ok(_)) -> [ 181 215 woof.field("message", "DatabaseAddedNewUser"), 182 216 woof.field("status", "ok"), 183 217 ] 184 - DatabaseAddedNewUser(Error(_)) -> [ 218 + DatabasePersistedNewUser(Error(_)) -> [ 185 219 woof.field("message", "DatabaseAddedNewUser"), 186 220 woof.field("status", "error"), 187 221 ] 188 - ServerGeneratedPassword(password) -> [ 222 + UserSubmittedSignUpForm(Ok(user)) -> [ 223 + woof.field("message", "UserSubmittedSignUpForm"), 224 + woof.field("status", "ok"), 225 + woof.field("user-email", user.email), 226 + ] 227 + UserSubmittedSignUpForm(Error(_)) -> [ 228 + woof.field("message", "UserSubmittedSignUpForm"), 229 + woof.field("status", "error"), 230 + ] 231 + ServerGeneratedPassword(password, user) -> [ 189 232 woof.field("message", "ServerGeneratedPassword"), 190 - woof.field("password", password), 233 + woof.field("user-email", user.email), 234 + woof.field("one-time-password", password), 191 235 ] 192 - UserSubmittedPassword(Ok(_)) -> [ 236 + UserSubmittedOneTimePassword(Ok(password)) -> [ 193 237 woof.field("message", "UserSubmittedPassword"), 194 238 woof.field("status", "ok"), 239 + woof.field("one-time-password", password), 195 240 ] 196 - UserSubmittedPassword(Error(_)) -> [ 241 + UserSubmittedOneTimePassword(Error(_)) -> [ 197 242 woof.field("message", "UserSubmittedPassword"), 198 243 woof.field("status", "error"), 199 244 ] 200 - ServerSentPassword(Ok(_)) -> [ 245 + ServerSentPassword(Ok(_), password_to_confirm: _, user: _) -> [ 201 246 woof.field("message", "ServerSentPassword"), 202 247 woof.field("status", "ok"), 203 248 ] 204 - ServerSentPassword(Error(smtp_error)) -> [ 249 + ServerSentPassword(Error(smtp_error), password_to_confirm: _, user: _) -> [ 205 250 woof.field("message", "ServerSentPassword"), 206 251 woof.field("status", "error"), 207 252 woof.field("details", string.inspect(smtp_error)), 208 253 ] 209 - TimerDismissedToast -> [] 254 + ServerVerifiedLogin(valid:, user:) -> [ 255 + woof.field("message", "ServerVerifiedLogin"), 256 + woof.field("user-email", user.email), 257 + woof.bool_field("valid", valid), 258 + ] 259 + UserClickedGotoSignUp -> [ 260 + woof.field("message", "UserClickedGotoSignUp"), 261 + ] 262 + UserClickedGotoLogIn -> [ 263 + woof.field("message", "UserClickedGotoLogIn"), 264 + ] 265 + TimerDismissedToast -> [ 266 + woof.field("message", "TimerDismissedToast"), 267 + ] 210 268 } 211 269 } 212 270 ··· 220 278 False -> Nil 221 279 } 222 280 223 - case message { 224 - UserSubmittedLoginForm(Error(form)) -> #( 225 - LoginModel(model.data, form), 281 + case message, model { 282 + // signup ------------------------------------------------------------------- 283 + UserClickedGotoSignUp, _ -> #( 284 + SignUpForm(model.data, signup_form()), 226 285 effect.none(), 227 286 ) 228 - UserSubmittedLoginForm(Ok(login)) -> #( 287 + UserSubmittedSignUpForm(Error(form)), _ -> #( 288 + SignUpForm(model.data, form), 289 + effect.none(), 290 + ) 291 + UserSubmittedSignUpForm(Ok(user)), _ -> #( 292 + model, 293 + generate_one_time_password(user), 294 + ) 295 + 296 + ServerGeneratedPassword(password_to_confirm, user), _ -> { 297 + #( 298 + model, 299 + send_one_time_password( 300 + password_to_confirm, 301 + user, 302 + model.data.smtp_environment, 303 + ), 304 + ) 305 + } 306 + 307 + ServerSentPassword(Ok(_), password_to_confirm:, user:), _ -> { 308 + let toast = 309 + toaster.Toast( 310 + title: None, 311 + message: "One time password was sent!", 312 + options: toast.default_options(toast.Info), 313 + ) 314 + let #(new_model, effect) = do_toast(toast, model) 315 + 316 + #( 317 + ConfirmOneTimePassword( 318 + data: ModelData(..model.data, toasts: new_model.data.toasts), 319 + user:, 320 + password_to_confirm:, 321 + form: one_time_password_form(), 322 + attempts_left: 3, 323 + ), 324 + effect, 325 + ) 326 + } 327 + ServerSentPassword(Error(_), password_to_confirm: _, user: _), _ -> { 328 + let toast = 329 + toaster.Toast( 330 + title: None, 331 + message: "I ran into a problem while sending the one time password", 332 + options: toast.default_options(toast.Danger), 333 + ) 334 + do_toast(toast, model) 335 + } 336 + 337 + // one time password matches, add a new user 338 + UserSubmittedOneTimePassword(Ok(password)), 339 + ConfirmOneTimePassword(password_to_confirm:, data:, user:, ..) 340 + if password == password_to_confirm 341 + -> #(model, create_new_user(user)) 342 + 343 + ServerCreatedNewUser(user), _ -> #( 344 + model, 345 + persist_new_user(user, model.data.database), 346 + ) 347 + DatabasePersistedNewUser(Ok(user)), _ -> #( 348 + LoggedIn(model.data, user), 349 + effect.none(), 350 + ) 351 + DatabasePersistedNewUser(Error(_)), _ -> { 352 + let toast = 353 + toaster.Toast( 354 + title: Some("Something went wrong"), 355 + message: "I ran into an issue :/", 356 + options: toast.default_options(toast.Danger), 357 + ) 358 + do_toast(toast, model) 359 + } 360 + 361 + // doesnt match, let the user retry up to 3 times 362 + UserSubmittedOneTimePassword(Ok(_)), 363 + ConfirmOneTimePassword(attempts_left:, ..) 364 + if attempts_left > 1 365 + -> { 366 + let attempts_left = attempts_left - 1 367 + let toast = 368 + toaster.Toast( 369 + title: Some("Invalid one time password"), 370 + message: "The one time password you entered was incorrect, you have " 371 + <> int.to_string(attempts_left) 372 + <> " attempts remaining.", 373 + options: toast.default_options(toast.Warning), 374 + ) 375 + let #(new_model, effect) = do_toast(toast, model) 376 + 377 + #( 378 + ConfirmOneTimePassword( 379 + ..model, 380 + data: ModelData(..model.data, toasts: new_model.data.toasts), 381 + attempts_left:, 382 + ), 383 + effect, 384 + ) 385 + } 386 + 387 + // user ran out of retries, send em back to the dark ages 388 + UserSubmittedOneTimePassword(Ok(_)), ConfirmOneTimePassword(..) -> { 389 + let toast = 390 + toaster.Toast( 391 + title: Some("Invalid"), 392 + message: "You entered an incorrect one time password to many times. Better luck next time :3", 393 + options: toast.default_options(toast.Danger), 394 + ) 395 + let #(new_model, effect) = do_toast(toast, model) 396 + 397 + #( 398 + SignUpForm( 399 + ModelData(..model.data, toasts: new_model.data.toasts), 400 + signup_form(), 401 + ), 402 + effect, 403 + ) 404 + } 405 + 406 + // invalid form -> send back with error messages generated by formal 407 + UserSubmittedOneTimePassword(Error(form)), ConfirmOneTimePassword(..) -> #( 408 + ConfirmOneTimePassword(..model, form:), 409 + effect.none(), 410 + ) 411 + // this one shouldnt happen 412 + UserSubmittedOneTimePassword(_), _ -> #(model, effect.none()) 413 + 414 + // login -------------------------------------------------------------------- 415 + UserClickedGotoLogIn, _ -> #( 416 + LoginForm(model.data, login_form()), 417 + effect.none(), 418 + ) 419 + UserSubmittedLoginForm(Error(form)), _ -> #( 420 + LoginForm(model.data, form), 421 + effect.none(), 422 + ) 423 + UserSubmittedLoginForm(Ok(login)), _ -> #( 229 424 model, 230 425 fetch_user_from_database(model.data, login), 231 426 ) 232 - DatabaseReturnedUser(Ok(Ok(user)), from_form) -> #( 427 + DatabaseReturnedUser(Ok(Ok(user)), from_form), _ -> #( 233 428 model, 234 429 verify_login(user, from_form), 235 430 ) 236 - DatabaseReturnedUser(Ok(Error(_)), _) -> { 237 - case model.data.allow_signups { 431 + DatabaseReturnedUser(Ok(Error(_)), _), _ -> { 432 + case model.data.configuration.allow_signups { 238 433 True -> { 239 434 let toast = 240 435 toaster.Toast( ··· 257 452 } 258 453 } 259 454 } 260 - DatabaseReturnedUser(Error(_), _) -> { 455 + DatabaseReturnedUser(Error(_), _), _ -> { 261 456 let toast = 262 457 toaster.Toast( 263 458 title: None, ··· 266 461 ) 267 462 do_toast(toast, model) 268 463 } 269 - ServerVerifiedLogin(valid: True, user:) -> #( 464 + ServerVerifiedLogin(valid: True, user:), _ -> #( 270 465 LoggedIn(model.data, user), 271 466 effect.none(), 272 467 ) 273 - ServerVerifiedLogin(valid: False, user: _) -> { 468 + ServerVerifiedLogin(valid: False, user: _), _ -> { 274 469 let toast = 275 470 toaster.Toast( 276 471 title: Some("Invalid login"), ··· 280 475 do_toast(toast, model) 281 476 } 282 477 283 - TimerDismissedToast -> { 478 + TimerDismissedToast, _ -> { 284 479 // the amount of toasts should stay small enough that this isnt an issue 285 480 let toasts = 286 481 model.data.toasts ··· 291 486 292 487 #(model, effect.none()) 293 488 } 489 + } 490 + } 294 491 295 - _ -> todo 296 - } 492 + /// create a new user from a `FormUser` 493 + /// 494 + /// `ServerCreatedNewUser` 495 + /// 496 + fn create_new_user(user: FormUser) { 497 + use dispatch <- effect.from 498 + 499 + user.new(user.email, user.hash_password(user.password)) 500 + |> ServerCreatedNewUser 501 + |> dispatch 502 + } 503 + 504 + /// persist a user to the database 505 + /// 506 + /// `DatabasePersistedNewUser` 507 + /// 508 + fn persist_new_user( 509 + user: user.User, 510 + database: sqlight.Connection, 511 + ) -> Effect(Message) { 512 + use dispatch <- effect.from 513 + 514 + database.add_user(user, database) 515 + |> DatabasePersistedNewUser 516 + |> dispatch 297 517 } 298 518 299 519 fn verify_login(user: user.User, from_form: FormUser) -> Effect(Message) { ··· 301 521 302 522 let form_password = user.hash_password(from_form.password) 303 523 304 - { form_password == user.password_hash && from_form.email == user.email } 524 + let correct_username_and_password = 525 + form_password == user.password_hash && from_form.email == user.email 526 + 527 + correct_username_and_password 305 528 |> ServerVerifiedLogin(user) 306 529 |> dispatch 307 530 } ··· 337 560 /// 338 561 /// `ServerGeneratedPassword` 339 562 /// 340 - fn generate_one_time_password() { 563 + fn generate_one_time_password(user: FormUser) { 341 564 use dispatch <- effect.from 342 565 343 566 crypto.strong_random_bytes(6) 344 567 |> bit_array.base16_encode() 345 - |> ServerGeneratedPassword 568 + |> ServerGeneratedPassword(user) 346 569 |> dispatch 347 570 } 348 571 ··· 351 574 /// `ServerSentPassword` 352 575 /// 353 576 fn send_one_time_password( 354 - password: String, 355 - user: user.User, 577 + one_time_password: String, 578 + user: FormUser, 356 579 smtp_environment: smtp.SmtpEnvironment, 357 580 ) -> Effect(Message) { 358 581 use dispatch <- effect.from() 359 582 360 - smtp.one_time_password(email: user.email, one_time_password: password) 583 + smtp.one_time_password(email: user.email, one_time_password:) 361 584 |> smtp.send_message(smtp_environment) 362 - |> result.replace(password) 363 - |> ServerSentPassword 585 + |> ServerSentPassword(one_time_password, user) 364 586 |> dispatch 365 587 } 366 588 ··· 369 591 fn view(model: Model) -> Element(Message) { 370 592 html.main([], [ 371 593 case model { 372 - LoginModel(data: _, form:) -> view_login_form(form:, busy: False) 594 + SignUpForm(data: _, form:) -> view_signup_form(form:) 595 + ConfirmOneTimePassword(form:, ..) -> 596 + view_one_time_password_form(form:, busy: False) 597 + LoginForm(data:, form:) -> 598 + view_login_form( 599 + form:, 600 + busy: False, 601 + allow_signups: data.configuration.allow_signups, 602 + ) 373 603 LoggedIn(data:, user:) -> view_logged_in(data, user) 374 604 }, 375 605 // button.button([event.on_click(SpawnToast)], [element.text("spawn toast")]), ··· 383 613 ]) 384 614 } 385 615 616 + fn view_signup_form(form form: Form(FormUser)) -> Element(Message) { 617 + let submitted = fn(fields) { 618 + form 619 + |> form.add_values(fields) 620 + |> form.run 621 + |> UserSubmittedSignUpForm 622 + } 623 + 624 + html.div( 625 + [ 626 + attribute.class("container vstack"), 627 + ], 628 + [ 629 + html.div([attribute.class("row")], [ 630 + card.card([attribute.class("col-4 offset-4")], [ 631 + card.header([], [html.h3([], [element.text("Sign up")])]), 632 + gform.form( 633 + [ 634 + attribute.method("POST"), 635 + event.on_submit(submitted), 636 + ], 637 + [ 638 + field_input(form, name: "email", kind: "text", label: "Email"), 639 + field_input( 640 + form, 641 + name: "email-confirmation", 642 + kind: "text", 643 + label: "Confirm email", 644 + ), 645 + field_input( 646 + form, 647 + name: "password", 648 + kind: "password", 649 + label: "Password", 650 + ), 651 + field_input( 652 + form, 653 + name: "password-confirmation", 654 + kind: "password", 655 + label: "Confirm password", 656 + ), 657 + html.div([], [html.input([attribute.type_("submit")])]), 658 + ], 659 + ), 660 + html.a([attribute.style("cursor", "pointer")], [ 661 + html.p([event.on_click(UserClickedGotoLogIn)], [ 662 + element.text("Log in"), 663 + ]), 664 + ]), 665 + ]), 666 + ]), 667 + ], 668 + ) 669 + } 670 + 386 671 fn view_logged_in(_data: ModelData, _user: user.User) -> Element(Message) { 387 672 html.div([], [ 388 673 html.h1([], [element.text("*hacker voice* im in!")]), 389 674 ]) 390 675 } 391 676 677 + fn view_one_time_password_form( 678 + form form: Form(String), 679 + busy busy: Bool, 680 + ) -> Element(Message) { 681 + let submitted = fn(fields) { 682 + form 683 + |> form.add_values(fields) 684 + |> form.run 685 + |> UserSubmittedOneTimePassword 686 + } 687 + // TODO: fix this having the value from the previous form in it for some reasn 688 + 689 + // TODO: fix minified oat missing 690 + // @layer components { 691 + // [aria-busy="true"] { 692 + // border: 2px solid var(--muted); 693 + 694 + html.div( 695 + [ 696 + attribute.class("container vstack"), 697 + attribute.aria_busy(busy), 698 + attribute.attribute("data-spinner", "large overlay"), 699 + ], 700 + [ 701 + html.div([attribute.class("row")], [ 702 + card.card([attribute.class("col-4 offset-4")], [ 703 + card.header([], [html.h3([], [element.text("Login")])]), 704 + gform.form( 705 + [ 706 + attribute.method("POST"), 707 + event.on_submit(submitted), 708 + ], 709 + [ 710 + field_input( 711 + form, 712 + name: "one_time_password", 713 + kind: "password", 714 + label: "One time password", 715 + ), 716 + html.div([], [html.input([attribute.type_("submit")])]), 717 + ], 718 + ), 719 + ]), 720 + ]), 721 + ], 722 + ) 723 + } 724 + 392 725 fn view_login_form( 393 726 form form: Form(FormUser), 394 727 busy busy: Bool, 728 + allow_signups allow_signups: Bool, 395 729 ) -> Element(Message) { 396 730 let submitted = fn(fields) { 397 731 form ··· 431 765 html.div([], [html.input([attribute.type_("submit")])]), 432 766 ], 433 767 ), 768 + case allow_signups { 769 + True -> 770 + html.a([attribute.style("cursor", "pointer")], [ 771 + html.p([event.on_click(UserClickedGotoSignUp)], [ 772 + element.text("Sign up"), 773 + ]), 774 + ]) 775 + False -> element.none() 776 + }, 434 777 ]), 435 778 ]), 436 779 ], ··· 451 794 }) 452 795 } 453 796 797 + fn signup_form() -> Form(FormUser) { 798 + form.new({ 799 + use email <- form.field("email", { form.parse_email }) 800 + use _ <- form.field("email-confirmation", { 801 + form.parse_email |> form.check_confirms(email) 802 + }) 803 + 804 + use password <- form.field("password", { 805 + form.parse_string 806 + |> form.check_string_length_more_than(8) 807 + }) 808 + 809 + use _ <- form.field("password-confirmation", { 810 + form.parse_string 811 + |> form.check_confirms(password) 812 + }) 813 + 814 + form.success(FormUser(email:, password:)) 815 + }) 816 + } 817 + 454 818 fn one_time_password_form() -> Form(String) { 455 819 form.new({ 456 - use password <- form.field("password", form.parse_string) 820 + use password <- form.field("one_time_password", form.parse_string) 457 821 form.success(password) 458 822 }) 459 823 }
+1 -1
src/eater/webserver.gleam
··· 227 227 csrf_token: token, 228 228 database:, 229 229 smtp_environment:, 230 - allow_signups: configuration.allow_signups, 230 + configuration: configuration, 231 231 ), 232 232 ) 233 233