Version 3 #40
|
@ -30,18 +30,19 @@ module Table =
|
||||||
|
|
||||||
open Npgsql.FSharp
|
open Npgsql.FSharp
|
||||||
|
|
||||||
/// Connection management for the Marten document store
|
/// Connection management for the document store
|
||||||
module DataConnection =
|
module DataConnection =
|
||||||
|
|
||||||
open Microsoft.Extensions.Configuration
|
open Microsoft.Extensions.Configuration
|
||||||
|
open Npgsql
|
||||||
|
|
||||||
/// The configuration from which a document store will be created
|
/// The data source for the document store
|
||||||
let mutable private config : IConfiguration option = None
|
let mutable private dataSource : NpgsqlDataSource option = None
|
||||||
|
|
||||||
/// Get the connection string
|
/// Get the connection string
|
||||||
let connection () =
|
let connection () =
|
||||||
match config with
|
match dataSource with
|
||||||
| Some cfg -> Sql.connect (cfg.GetConnectionString "PostgreSQL")
|
| Some ds -> ds.OpenConnection () |> Sql.existingConnection
|
||||||
| None -> invalidOp "Connection.setUp() must be called before accessing the database"
|
| None -> invalidOp "Connection.setUp() must be called before accessing the database"
|
||||||
|
|
||||||
/// Create tables
|
/// Create tables
|
||||||
|
@ -72,7 +73,7 @@ module DataConnection =
|
||||||
|
|
||||||
/// Set up the data connection from the given configuration
|
/// Set up the data connection from the given configuration
|
||||||
let setUp (cfg : IConfiguration) = backgroundTask {
|
let setUp (cfg : IConfiguration) = backgroundTask {
|
||||||
config <- Some cfg
|
dataSource <- Some (NpgsqlDataSource.Create (cfg.GetConnectionString "PostgreSQL"))
|
||||||
do! createTables ()
|
do! createTables ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
@ -16,11 +15,10 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FSharp.SystemTextJson" Version="0.19.13" />
|
<PackageReference Include="FSharp.SystemTextJson" Version="0.19.13" />
|
||||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
|
||||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.0.0" />
|
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.0.0" />
|
||||||
<PackageReference Include="Npgsql" Version="6.0.6" />
|
<PackageReference Include="Npgsql" Version="7.0.0" />
|
||||||
<PackageReference Include="Npgsql.FSharp" Version="5.3.0" />
|
<PackageReference Include="Npgsql.FSharp" Version="5.5.0" />
|
||||||
<PackageReference Include="Npgsql.NodaTime" Version="6.0.6" />
|
<PackageReference Include="Npgsql.NodaTime" Version="7.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<DebugType>embedded</DebugType>
|
<DebugType>embedded</DebugType>
|
||||||
<AssemblyVersion>3.0.0.0</AssemblyVersion>
|
<AssemblyVersion>3.0.0.0</AssemblyVersion>
|
||||||
|
|
|
@ -14,9 +14,8 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||||
<PackageReference Include="Markdig" Version="0.30.3" />
|
<PackageReference Include="Markdig" Version="0.30.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
|
||||||
<PackageReference Include="NodaTime" Version="3.1.2" />
|
<PackageReference Include="NodaTime" Version="3.1.2" />
|
||||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -13,7 +12,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" />
|
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" />
|
||||||
<PackageReference Include="RethinkDb.Driver.FSharp" Version="0.9.0-beta-07" />
|
<PackageReference Include="RethinkDb.Driver.FSharp" Version="0.9.0-beta-07" />
|
||||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -114,10 +114,94 @@ module Helpers =
|
||||||
|
|
||||||
open System
|
open System
|
||||||
open JobsJobsJobs.Data
|
open JobsJobsJobs.Data
|
||||||
|
open JobsJobsJobs.ViewModels
|
||||||
|
|
||||||
|
/// Handlers for /citizen routes
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Citizen =
|
||||||
|
|
||||||
|
open Microsoft.Extensions.Configuration
|
||||||
|
|
||||||
|
/// Support module for /citizen routes
|
||||||
|
module private Support =
|
||||||
|
|
||||||
|
/// The challenge questions and answers from the configuration
|
||||||
|
let mutable private challenges : (string * string)[] option = None
|
||||||
|
|
||||||
|
/// The challenge questions and answers
|
||||||
|
let questions ctx =
|
||||||
|
match challenges with
|
||||||
|
| Some it -> it
|
||||||
|
| None ->
|
||||||
|
let qs = (config ctx).GetSection "ChallengeQuestions"
|
||||||
|
let qAndA =
|
||||||
|
seq {
|
||||||
|
for idx in 0..4 do
|
||||||
|
let section = qs.GetSection(string idx)
|
||||||
|
yield section["Question"], section["Answer"]
|
||||||
|
}
|
||||||
|
|> Array.ofSeq
|
||||||
|
challenges <- Some qAndA
|
||||||
|
qAndA
|
||||||
|
|
||||||
|
// GET: /citizen/log-on
|
||||||
|
let logOn : HttpHandler =
|
||||||
|
render "Log On" (Citizen.logOn { ErrorMessage = None; Email = ""; Password = "" })
|
||||||
|
|
||||||
|
// GET: /citizen/register
|
||||||
|
let register : HttpHandler = fun next ctx -> task {
|
||||||
|
// Get two different indexes for NA-knowledge challenge questions
|
||||||
|
let q1Index = System.Random.Shared.Next(0, 5)
|
||||||
|
let mutable q2Index = System.Random.Shared.Next(0, 5)
|
||||||
|
while q1Index = q2Index do
|
||||||
|
q2Index <- System.Random.Shared.Next(0, 5)
|
||||||
|
let qAndA = Support.questions ctx
|
||||||
|
return! render "Register"
|
||||||
|
(Citizen.register (fst qAndA[q1Index]) (fst qAndA[q2Index])
|
||||||
|
{ RegisterViewModel.empty with Question1Index = q1Index; Question2Index = q2Index }) next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: /citizen/register
|
||||||
|
let doRegistration : HttpHandler = fun next ctx -> task {
|
||||||
|
let! form = ctx.BindFormAsync<RegisterViewModel> ()
|
||||||
|
// FIXME: stopped here; add validation
|
||||||
|
if form.Password.Length < 8 then
|
||||||
|
return! RequestErrors.BAD_REQUEST "Password out of range" next ctx
|
||||||
|
else
|
||||||
|
let now = now ctx
|
||||||
|
let noPass =
|
||||||
|
{ Citizen.empty with
|
||||||
|
Id = CitizenId.create ()
|
||||||
|
Email = form.Email
|
||||||
|
FirstName = form.FirstName
|
||||||
|
LastName = form.LastName
|
||||||
|
DisplayName = noneIfBlank form.DisplayName
|
||||||
|
JoinedOn = now
|
||||||
|
LastSeenOn = now
|
||||||
|
}
|
||||||
|
let citizen = { noPass with PasswordHash = Auth.Passwords.hash noPass form.Password }
|
||||||
|
let security =
|
||||||
|
{ SecurityInfo.empty with
|
||||||
|
Id = citizen.Id
|
||||||
|
AccountLocked = true
|
||||||
|
Token = Some (Auth.createToken citizen)
|
||||||
|
TokenUsage = Some "confirm"
|
||||||
|
TokenExpires = Some (now + (Duration.FromDays 3))
|
||||||
|
}
|
||||||
|
let! success = Citizens.register citizen security
|
||||||
|
if success then
|
||||||
|
let! emailResponse = Email.sendAccountConfirmation citizen security
|
||||||
|
let logFac = logger ctx
|
||||||
|
let log = logFac.CreateLogger "JobsJobsJobs.Api.Handlers.Citizen"
|
||||||
|
log.LogInformation $"Confirmation e-mail for {citizen.Email} received {emailResponse}"
|
||||||
|
return! ok next ctx
|
||||||
|
else
|
||||||
|
return! RequestErrors.CONFLICT "" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
/// Handlers for /api/citizen routes
|
/// Handlers for /api/citizen routes
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Citizen =
|
module CitizenApi =
|
||||||
|
|
||||||
// POST: /api/citizen/register
|
// POST: /api/citizen/register
|
||||||
let register : HttpHandler = fun next ctx -> task {
|
let register : HttpHandler = fun next ctx -> task {
|
||||||
|
@ -510,23 +594,30 @@ open Giraffe.EndpointRouting
|
||||||
/// All available endpoints for the application
|
/// All available endpoints for the application
|
||||||
let allEndpoints = [
|
let allEndpoints = [
|
||||||
GET_HEAD [ route "/" Home.home ]
|
GET_HEAD [ route "/" Home.home ]
|
||||||
|
subRoute "/citizen" [
|
||||||
|
GET_HEAD [
|
||||||
|
route "/log-on" Citizen.logOn
|
||||||
|
route "/register" Citizen.register
|
||||||
|
]
|
||||||
|
POST [ route "/register" Citizen.doRegistration ]
|
||||||
|
]
|
||||||
GET_HEAD [ route "/how-it-works" Home.howItWorks ]
|
GET_HEAD [ route "/how-it-works" Home.howItWorks ]
|
||||||
GET_HEAD [ route "/privacy-policy" Home.privacyPolicy ]
|
GET_HEAD [ route "/privacy-policy" Home.privacyPolicy ]
|
||||||
GET_HEAD [ route "/terms-of-service" Home.termsOfService ]
|
GET_HEAD [ route "/terms-of-service" Home.termsOfService ]
|
||||||
subRoute "/api" [
|
subRoute "/api" [
|
||||||
subRoute "/citizen" [
|
subRoute "/citizen" [
|
||||||
GET_HEAD [ routef "/%O" Citizen.get ]
|
GET_HEAD [ routef "/%O" CitizenApi.get ]
|
||||||
PATCH [
|
PATCH [
|
||||||
route "/account" Citizen.account
|
route "/account" CitizenApi.account
|
||||||
route "/confirm" Citizen.confirmToken
|
route "/confirm" CitizenApi.confirmToken
|
||||||
]
|
]
|
||||||
POST [
|
POST [
|
||||||
route "/log-on" Citizen.logOn
|
route "/log-on" CitizenApi.logOn
|
||||||
route "/register" Citizen.register
|
route "/register" CitizenApi.register
|
||||||
]
|
]
|
||||||
DELETE [
|
DELETE [
|
||||||
route "" Citizen.delete
|
route "" CitizenApi.delete
|
||||||
route "/deny" Citizen.denyToken
|
route "/deny" CitizenApi.denyToken
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
GET_HEAD [ route "/continents" Continent.all ]
|
GET_HEAD [ route "/continents" Continent.all ]
|
||||||
|
|
|
@ -34,11 +34,10 @@
|
||||||
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
|
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
|
||||||
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.8.4" />
|
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.8.4" />
|
||||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.FSharpLu.Json" Version="0.11.7" />
|
<PackageReference Include="Microsoft.FSharpLu.Json" Version="0.11.7" />
|
||||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" />
|
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.22.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.22.0" />
|
||||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -12,3 +12,50 @@ type LogOnViewModel =
|
||||||
/// The password of the user attempting to log on
|
/// The password of the user attempting to log on
|
||||||
Password : string
|
Password : string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// View model for the registration page
|
||||||
|
type RegisterViewModel =
|
||||||
|
{ /// The user's first name
|
||||||
|
FirstName : string
|
||||||
|
|
||||||
|
/// The user's last name
|
||||||
|
LastName : string
|
||||||
|
|
||||||
|
/// The user's display name
|
||||||
|
DisplayName : string option
|
||||||
|
|
||||||
|
/// The user's e-mail address
|
||||||
|
Email : string
|
||||||
|
|
||||||
|
/// The user's desired password
|
||||||
|
Password : string
|
||||||
|
|
||||||
|
/// The index of the first question asked
|
||||||
|
Question1Index : int
|
||||||
|
|
||||||
|
/// The answer for the first question asked
|
||||||
|
Question1Answer : string
|
||||||
|
|
||||||
|
/// The index of the second question asked
|
||||||
|
Question2Index : int
|
||||||
|
|
||||||
|
/// The answer for the second question asked
|
||||||
|
Question2Answer : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Support for the registration page view model
|
||||||
|
module RegisterViewModel =
|
||||||
|
|
||||||
|
/// An empty view model
|
||||||
|
let empty =
|
||||||
|
{ FirstName = ""
|
||||||
|
LastName = ""
|
||||||
|
DisplayName = None
|
||||||
|
Email = ""
|
||||||
|
Password = ""
|
||||||
|
Question1Index = 0
|
||||||
|
Question1Answer = ""
|
||||||
|
Question2Index = 0
|
||||||
|
Question2Answer = ""
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
module JobsJobsJobs.Views.Citizen
|
module JobsJobsJobs.Views.Citizen
|
||||||
|
|
||||||
open Giraffe.ViewEngine
|
open Giraffe.ViewEngine
|
||||||
|
open Giraffe.ViewEngine.Htmx
|
||||||
open JobsJobsJobs.ViewModels
|
open JobsJobsJobs.ViewModels
|
||||||
|
|
||||||
/// The log on page
|
/// The log on page
|
||||||
|
@ -20,29 +21,29 @@ let logOn (m : LogOnViewModel) =
|
||||||
rawText " before you may log on."
|
rawText " before you may log on."
|
||||||
]
|
]
|
||||||
| None -> ()
|
| None -> ()
|
||||||
form [ _class "row g-3 pb-3"] [
|
form [ _class "row g-3 pb-3"; _hxPost "/citizen/log-on" ] [
|
||||||
div [ _class "col-12 col-md-6" ] [
|
div [ _class "col-12 col-md-6" ] [
|
||||||
div [ _class "form-floating" ] [
|
div [ _class "form-floating" ] [
|
||||||
input [ _type "email"
|
input [ _type "email"
|
||||||
_id "email"
|
|
||||||
_class "form-control"
|
_class "form-control"
|
||||||
|
_id (nameof m.Email)
|
||||||
_name (nameof m.Email)
|
_name (nameof m.Email)
|
||||||
_placeholder "E-mail Address"
|
_placeholder "E-mail Address"
|
||||||
_value m.Email
|
_value m.Email
|
||||||
_required
|
_required
|
||||||
_autofocus ]
|
_autofocus ]
|
||||||
label [ _class "jjj-required"; _for "email" ] [ rawText "E-mail Address" ]
|
label [ _class "jjj-required"; _for (nameof m.Email) ] [ rawText "E-mail Address" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-md-6" ] [
|
div [ _class "col-12 col-md-6" ] [
|
||||||
div [ _class "form-floating" ] [
|
div [ _class "form-floating" ] [
|
||||||
input [ _type "password"
|
input [ _type "password"
|
||||||
_id "password"
|
|
||||||
_class "form-control"
|
_class "form-control"
|
||||||
|
_id (nameof m.Password)
|
||||||
_name (nameof m.Password)
|
_name (nameof m.Password)
|
||||||
_placeholder "Password"
|
_placeholder "Password"
|
||||||
_required ]
|
_required ]
|
||||||
label [ _class "jjj-required"; _for "password" ] [ rawText "Password" ]
|
label [ _class "jjj-required"; _for (nameof m.Password) ] [ rawText "Password" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [
|
||||||
|
@ -58,3 +59,97 @@ let logOn (m : LogOnViewModel) =
|
||||||
rawText "Forgot your password? "; a [ _href "/citizen/forgot-password" ] [ rawText "Request a reset." ]
|
rawText "Forgot your password? "; a [ _href "/citizen/forgot-password" ] [ rawText "Request a reset." ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/// The registration page
|
||||||
|
let register q1 q2 (m : RegisterViewModel) =
|
||||||
|
article [] [
|
||||||
|
h3 [ _class "pb-3" ] [ rawText "Register" ]
|
||||||
|
form [ _class "row g-3"; _hxPost "/citizen/register" ] [
|
||||||
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _class "form-control"; _id (nameof m.FirstName); _name (nameof m.FirstName)
|
||||||
|
_value m.FirstName; _placeholder "First Name"; _required; _autofocus ]
|
||||||
|
label [ _class "jjj-required"; _for (nameof m.FirstName) ] [ rawText "First Name" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _class "form-control"; _id (nameof m.LastName); _name (nameof m.LastName)
|
||||||
|
_value m.LastName; _placeholder "Last Name"; _required ]
|
||||||
|
label [ _class "jjj-required"; _for (nameof m.LastName) ] [ rawText "Last Name" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _class "form-control"; _id (nameof m.DisplayName)
|
||||||
|
_name (nameof m.DisplayName); _value (defaultArg m.DisplayName "")
|
||||||
|
_placeholder "Display Name" ]
|
||||||
|
label [ _for (nameof m.DisplayName) ] [ rawText "Display Name" ]
|
||||||
|
div [ _class "form-text" ] [ em [] [ rawText "Optional; overrides first/last for display" ] ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "email"; _class "form-control"; _id (nameof m.Email); _name (nameof m.Email)
|
||||||
|
_value m.Email; _placeholder "E-mail Address"; _required ]
|
||||||
|
label [ _class "jjj-required"; _for (nameof m.Email) ] [ rawText "E-mail Address" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "password"; _class "form-control"; _id (nameof m.Password); _name (nameof m.Password)
|
||||||
|
_placeholder "Password"; _minlength "8"; _required ]
|
||||||
|
label [ _class "jjj-required"; _for (nameof m.Password) ] [ rawText "Password" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "password"; _class "form-control"; _id "ConfirmPassword"
|
||||||
|
_placeholder "Confirm Password"; _minlength "8"; _required ]
|
||||||
|
label [ _class "jjj-required"; _for "ConfirmPassword" ] [ rawText "Confirm Password" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-12" ] [
|
||||||
|
hr []
|
||||||
|
p [ _class "mb-0 text-muted fst-italic" ] [
|
||||||
|
rawText "Before your account request is through, you must answer these questions two…"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-xl-6" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _class "form-control"; _id (nameof m.Question1Answer)
|
||||||
|
_name (nameof m.Question1Answer); _value m.Question1Answer; _placeholder "Question 1"
|
||||||
|
_maxlength "30"; _required ]
|
||||||
|
label [ _class "jjj-required"; _for (nameof m.Question1Answer) ] [ str q1 ]
|
||||||
|
]
|
||||||
|
input [ _type "hidden"; _name (nameof m.Question1Index); _value (string m.Question1Index ) ]
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-xl-6" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _class "form-control"; _id (nameof m.Question2Answer)
|
||||||
|
_name (nameof m.Question2Answer); _value m.Question2Answer; _placeholder "Question 2"
|
||||||
|
_maxlength "30"; _required ]
|
||||||
|
label [ _class "jjj-required"; _for (nameof m.Question2Answer) ] [ str q2 ]
|
||||||
|
]
|
||||||
|
input [ _type "hidden"; _name (nameof m.Question2Index); _value (string m.Question2Index ) ]
|
||||||
|
]
|
||||||
|
div [ _class "col-12" ] [
|
||||||
|
button [ _type "submit"; _class "btn btn-primary" ] [
|
||||||
|
i [ _class "mdi mdi-content-save-outline" ] []; rawText " Save"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
script [] [ rawText """
|
||||||
|
const pw = document.getElementById("Password")
|
||||||
|
const pwConfirm = document.getElementById("ConfirmPassword")
|
||||||
|
pwConfirm.addEventListener("input", () => {
|
||||||
|
if (!pw.validity.valid) {
|
||||||
|
pwConfirm.setCustomValidity("")
|
||||||
|
} else if (!pwConfirm.validity.valueMissing && pw.value !== pwConfirm.value) {
|
||||||
|
pwConfirm.setCustomValidity("Confirmation password does not match")
|
||||||
|
} else {
|
||||||
|
pwConfirm.setCustomValidity("")
|
||||||
|
}
|
||||||
|
})"""
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user