Version 3 #40
|
@ -17,10 +17,10 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Domain", "JobsJobsJobs\Doma
|
||||||
EndProject
|
EndProject
|
||||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Api", "JobsJobsJobs\Server\JobsJobsJobs.Server.fsproj", "{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}"
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Api", "JobsJobsJobs\Server\JobsJobsJobs.Server.fsproj", "{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "JobsJobsJobs.Data", "JobsJobsJobs\JobsJobsJobs.Data\JobsJobsJobs.Data.fsproj", "{30CC1E7C-A843-4DAC-9058-E7C6ACBCE85D}"
|
|
||||||
EndProject
|
|
||||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "JobsJobsJobs.V3Migration", "JobsJobsJobs\JobsJobsJobs.V3Migration\JobsJobsJobs.V3Migration.fsproj", "{DC3E225D-9720-44E8-86AE-DEE71262C9F0}"
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "JobsJobsJobs.V3Migration", "JobsJobsJobs\JobsJobsJobs.V3Migration\JobsJobsJobs.V3Migration.fsproj", "{DC3E225D-9720-44E8-86AE-DEE71262C9F0}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "JobsJobsJobs.Data", "JobsJobsJobs\Data\JobsJobsJobs.Data.fsproj", "{BD2A0986-0B08-4C9F-94D4-EA5EF01EC03F}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -35,14 +35,14 @@ Global
|
||||||
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{30CC1E7C-A843-4DAC-9058-E7C6ACBCE85D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{30CC1E7C-A843-4DAC-9058-E7C6ACBCE85D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{30CC1E7C-A843-4DAC-9058-E7C6ACBCE85D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{30CC1E7C-A843-4DAC-9058-E7C6ACBCE85D}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{DC3E225D-9720-44E8-86AE-DEE71262C9F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{DC3E225D-9720-44E8-86AE-DEE71262C9F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{DC3E225D-9720-44E8-86AE-DEE71262C9F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{DC3E225D-9720-44E8-86AE-DEE71262C9F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{DC3E225D-9720-44E8-86AE-DEE71262C9F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{DC3E225D-9720-44E8-86AE-DEE71262C9F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{DC3E225D-9720-44E8-86AE-DEE71262C9F0}.Release|Any CPU.Build.0 = Release|Any CPU
|
{DC3E225D-9720-44E8-86AE-DEE71262C9F0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{BD2A0986-0B08-4C9F-94D4-EA5EF01EC03F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{BD2A0986-0B08-4C9F-94D4-EA5EF01EC03F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BD2A0986-0B08-4C9F-94D4-EA5EF01EC03F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{BD2A0986-0B08-4C9F-94D4-EA5EF01EC03F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -53,7 +53,7 @@ Global
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{C81278DA-DA97-4E55-AB39-4B88565B615D} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
{C81278DA-DA97-4E55-AB39-4B88565B615D} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
||||||
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
||||||
{30CC1E7C-A843-4DAC-9058-E7C6ACBCE85D} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
|
||||||
{DC3E225D-9720-44E8-86AE-DEE71262C9F0} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
{DC3E225D-9720-44E8-86AE-DEE71262C9F0} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
||||||
|
{BD2A0986-0B08-4C9F-94D4-EA5EF01EC03F} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
@ -115,7 +115,7 @@ export default {
|
||||||
* Confirm an account by verifying a token they received via e-mail
|
* Confirm an account by verifying a token they received via e-mail
|
||||||
*
|
*
|
||||||
* @param token The token to be verified
|
* @param token The token to be verified
|
||||||
* @return True if the token is value, false if it is not, or an error message if one is encountered
|
* @return True if the token is valid, false if it is not, or an error message if one is encountered
|
||||||
*/
|
*/
|
||||||
confirmToken: async (token : string) : Promise<boolean | string> => {
|
confirmToken: async (token : string) : Promise<boolean | string> => {
|
||||||
const resp = await apiResult<Valid>(
|
const resp = await apiResult<Valid>(
|
||||||
|
@ -125,6 +125,20 @@ export default {
|
||||||
return resp.valid
|
return resp.valid
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deny an account after verifying the token they received via e-mail
|
||||||
|
*
|
||||||
|
* @param token The token to be verified
|
||||||
|
* @return True if the token is valid, false if it is not, or an error message if one is encountered
|
||||||
|
*/
|
||||||
|
denyAccount: async (token : string) : Promise<boolean | string> => {
|
||||||
|
const resp = await apiResult<Valid>(
|
||||||
|
await fetch(apiUrl("citizen/deny"), reqInit("DELETE", undefined, { token })), "denying account")
|
||||||
|
if (typeof resp === "string") return resp
|
||||||
|
if (typeof resp === "undefined") return false
|
||||||
|
return resp.valid
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a citizen on
|
* Log a citizen on
|
||||||
*
|
*
|
||||||
|
|
|
@ -67,6 +67,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
component: () => import(/* webpackChunkName: "logon" */ "../views/citizen/ConfirmRegistration.vue"),
|
component: () => import(/* webpackChunkName: "logon" */ "../views/citizen/ConfirmRegistration.vue"),
|
||||||
meta: { auth: false, title: "Account Confirmation" }
|
meta: { auth: false, title: "Account Confirmation" }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/citizen/deny/:token",
|
||||||
|
name: "DenyRegistration",
|
||||||
|
component: () => import(/* webpackChunkName: "deny" */ "../views/citizen/DenyRegistration.vue"),
|
||||||
|
meta: { auth: false, title: "Account Deletion" }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/citizen/log-on",
|
path: "/citizen/log-on",
|
||||||
name: "LogOn",
|
name: "LogOn",
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<p v-else>
|
<p v-else>
|
||||||
The confirmation token did not match any pending accounts. Confirmation tokens are only valid for 3 days; if
|
The confirmation token did not match any pending accounts. Confirmation tokens are only valid for 3 days; if
|
||||||
the token expired, you will need to re-register,
|
the token expired, you will need to re-register,
|
||||||
which <router-link to="/citzen/register">you can do here</router-link>.
|
which <router-link to="/citizen/register">you can do here</router-link>.
|
||||||
</p>
|
</p>
|
||||||
</load-data>
|
</load-data>
|
||||||
</article>
|
</article>
|
||||||
|
|
37
src/JobsJobsJobs/App/src/views/citizen/DenyRegistration.vue
Normal file
37
src/JobsJobsJobs/App/src/views/citizen/DenyRegistration.vue
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<article>
|
||||||
|
<h3 class="pb-3">Account Deletion</h3>
|
||||||
|
<load-data :load="denyAccount">
|
||||||
|
<p v-if="isDeleted">
|
||||||
|
The account was deleted successfully; sorry for the trouble.
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
The confirmation token did not match any pending accounts; if this was an inadvertently created account, it has
|
||||||
|
likely already been deleted.
|
||||||
|
</p>
|
||||||
|
</load-data>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { useRoute } from "vue-router"
|
||||||
|
|
||||||
|
import api from "@/api"
|
||||||
|
import LoadData from "@/components/LoadData.vue"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
/** Whether the account was deleted */
|
||||||
|
const isDeleted = ref(false)
|
||||||
|
|
||||||
|
/** Deny the account after confirming the token */
|
||||||
|
const denyAccount = async (errors: string[]) => {
|
||||||
|
const resp = await api.citizen.denyAccount(route.params.token as string)
|
||||||
|
if (typeof resp === "string") {
|
||||||
|
errors.push(resp)
|
||||||
|
} else {
|
||||||
|
isDeleted.value = resp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,4 +1,4 @@
|
||||||
namespace JobsJobsJobs.Data
|
namespace JobsJobsJobs.Data
|
||||||
|
|
||||||
/// Constants for tables used by Jobs, Jobs, Jobs
|
/// Constants for tables used by Jobs, Jobs, Jobs
|
||||||
module Table =
|
module Table =
|
||||||
|
@ -138,10 +138,18 @@ open JobsJobsJobs.Domain
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Citizens =
|
module Citizens =
|
||||||
|
|
||||||
/// Delete a citizen by their ID
|
open NodaTime
|
||||||
let deleteById citizenId = backgroundTask {
|
|
||||||
|
/// The last time a token purge check was run
|
||||||
|
let mutable private lastPurge = Instant.MinValue
|
||||||
|
|
||||||
|
/// Lock access to the above
|
||||||
|
let private locker = obj ()
|
||||||
|
|
||||||
|
/// Delete a citizen by their ID using the given connection properties
|
||||||
|
let private doDeleteById citizenId connProps = backgroundTask {
|
||||||
let! _ =
|
let! _ =
|
||||||
connection ()
|
connProps
|
||||||
|> Sql.query $"
|
|> Sql.query $"
|
||||||
DELETE FROM {Table.Success} WHERE data ->> 'citizenId' = @id;
|
DELETE FROM {Table.Success} WHERE data ->> 'citizenId' = @id;
|
||||||
DELETE FROM {Table.Listing} WHERE data ->> 'citizenId' = @id;
|
DELETE FROM {Table.Listing} WHERE data ->> 'citizenId' = @id;
|
||||||
|
@ -151,13 +159,9 @@ module Citizens =
|
||||||
()
|
()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a citizen by their ID
|
/// Delete a citizen by their ID
|
||||||
let findById citizenId = backgroundTask {
|
let deleteById citizenId =
|
||||||
match! connection () |> getDocument<Citizen> Table.Citizen (CitizenId.toString citizenId) with
|
doDeleteById citizenId (connection ())
|
||||||
| Some c when not c.IsLegacy -> return Some c
|
|
||||||
| Some _
|
|
||||||
| None -> return None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save a citizen
|
/// Save a citizen
|
||||||
let private saveCitizen (citizen : Citizen) connProps =
|
let private saveCitizen (citizen : Citizen) connProps =
|
||||||
|
@ -167,6 +171,38 @@ module Citizens =
|
||||||
let private saveSecurity (security : SecurityInfo) connProps =
|
let private saveSecurity (security : SecurityInfo) connProps =
|
||||||
saveDocument Table.SecurityInfo (CitizenId.toString security.Id) connProps (mkDoc security)
|
saveDocument Table.SecurityInfo (CitizenId.toString security.Id) connProps (mkDoc security)
|
||||||
|
|
||||||
|
/// Purge expired tokens
|
||||||
|
let private purgeExpiredTokens now = backgroundTask {
|
||||||
|
let connProps = connection ()
|
||||||
|
let! info =
|
||||||
|
Sql.query $"SELECT * FROM {Table.SecurityInfo} WHERE data ->> 'tokenExpires' IS NOT NULL" connProps
|
||||||
|
|> Sql.executeAsync toDocument<SecurityInfo>
|
||||||
|
for expired in info |> List.filter (fun it -> it.TokenExpires.Value < now) do
|
||||||
|
if expired.TokenUsage.Value = "confirm" then
|
||||||
|
// Unconfirmed account; delete the entire thing
|
||||||
|
do! doDeleteById expired.Id connProps
|
||||||
|
else
|
||||||
|
// Some other use; just clear the token
|
||||||
|
do! saveSecurity { expired with Token = None; TokenUsage = None; TokenExpires = None } connProps
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check for tokens to purge if it's been more than 10 minutes since we last checked
|
||||||
|
let private checkForPurge skipCheck =
|
||||||
|
lock locker (fun () -> backgroundTask {
|
||||||
|
let now = SystemClock.Instance.GetCurrentInstant ()
|
||||||
|
if skipCheck || (now - lastPurge).TotalMinutes >= 10 then
|
||||||
|
do! purgeExpiredTokens now
|
||||||
|
lastPurge <- now
|
||||||
|
})
|
||||||
|
|
||||||
|
/// Find a citizen by their ID
|
||||||
|
let findById citizenId = backgroundTask {
|
||||||
|
match! connection () |> getDocument<Citizen> Table.Citizen (CitizenId.toString citizenId) with
|
||||||
|
| Some c when not c.IsLegacy -> return Some c
|
||||||
|
| Some _
|
||||||
|
| None -> return None
|
||||||
|
}
|
||||||
|
|
||||||
/// Save a citizen
|
/// Save a citizen
|
||||||
let save citizen =
|
let save citizen =
|
||||||
saveCitizen citizen (connection ())
|
saveCitizen citizen (connection ())
|
||||||
|
@ -182,20 +218,8 @@ module Citizens =
|
||||||
do! txn.CommitAsync ()
|
do! txn.CommitAsync ()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Purge expired tokens
|
/// Try to find the security information matching a confirmation token
|
||||||
let private purgeExpiredTokens now = backgroundTask {
|
let private tryConfirmToken token connProps = backgroundTask {
|
||||||
let connProps = connection ()
|
|
||||||
let! info =
|
|
||||||
Sql.query $"SELECT * FROM {Table.SecurityInfo} WHERE data ->> 'tokenExpires' IS NOT NULL" connProps
|
|
||||||
|> Sql.executeAsync toDocument<SecurityInfo>
|
|
||||||
for expired in info |> List.filter (fun it -> it.TokenExpires.Value < now) do
|
|
||||||
do! saveSecurity { expired with Token = None; TokenUsage = None; TokenExpires = None } connProps
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Confirm a citizen's account
|
|
||||||
let confirmAccount token now = backgroundTask {
|
|
||||||
do! purgeExpiredTokens now
|
|
||||||
let connProps = connection ()
|
|
||||||
let! tryInfo =
|
let! tryInfo =
|
||||||
connProps
|
connProps
|
||||||
|> Sql.query $"
|
|> Sql.query $"
|
||||||
|
@ -205,7 +229,14 @@ module Citizens =
|
||||||
AND data ->> 'tokenUsage' = 'confirm'"
|
AND data ->> 'tokenUsage' = 'confirm'"
|
||||||
|> Sql.parameters [ "@token", Sql.string token ]
|
|> Sql.parameters [ "@token", Sql.string token ]
|
||||||
|> Sql.executeAsync toDocument<SecurityInfo>
|
|> Sql.executeAsync toDocument<SecurityInfo>
|
||||||
match List.tryHead tryInfo with
|
return List.tryHead tryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirm a citizen's account
|
||||||
|
let confirmAccount token = backgroundTask {
|
||||||
|
do! checkForPurge true
|
||||||
|
let connProps = connection ()
|
||||||
|
match! tryConfirmToken token connProps with
|
||||||
| Some info ->
|
| Some info ->
|
||||||
do! saveSecurity { info with AccountLocked = false; Token = None; TokenUsage = None; TokenExpires = None }
|
do! saveSecurity { info with AccountLocked = false; Token = None; TokenUsage = None; TokenExpires = None }
|
||||||
connProps
|
connProps
|
||||||
|
@ -213,8 +244,20 @@ module Citizens =
|
||||||
| None -> return false
|
| None -> return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deny a citizen's account (user-initiated; used if someone used their e-mail address without their consent)
|
||||||
|
let denyAccount token = backgroundTask {
|
||||||
|
do! checkForPurge true
|
||||||
|
let connProps = connection ()
|
||||||
|
match! tryConfirmToken token connProps with
|
||||||
|
| Some info ->
|
||||||
|
do! doDeleteById info.Id connProps
|
||||||
|
return true
|
||||||
|
| None -> return false
|
||||||
|
}
|
||||||
|
|
||||||
/// Attempt a user log on
|
/// Attempt a user log on
|
||||||
let tryLogOn email (pwCheck : string -> bool) now = backgroundTask {
|
let tryLogOn email (pwCheck : string -> bool) now = backgroundTask {
|
||||||
|
do! checkForPurge false
|
||||||
let connProps = connection ()
|
let connProps = connection ()
|
||||||
let! tryCitizen =
|
let! tryCitizen =
|
||||||
connProps
|
connProps
|
|
@ -1,4 +1,4 @@
|
||||||
module JobsJobsJobs.Data.Json
|
module JobsJobsJobs.Data.Json
|
||||||
|
|
||||||
open System.Text.Json
|
open System.Text.Json
|
||||||
open System.Text.Json.Serialization
|
open System.Text.Json.Serialization
|
|
@ -17,7 +17,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\JobsJobsJobs.Data\JobsJobsJobs.Data.fsproj" />
|
<ProjectReference Include="..\Data\JobsJobsJobs.Data.fsproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
46
src/JobsJobsJobs/Server/Email.fs
Normal file
46
src/JobsJobsJobs/Server/Email.fs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
module JobsJobsJobs.Api.Email
|
||||||
|
|
||||||
|
open JobsJobsJobs.Domain
|
||||||
|
open MailKit.Net.Smtp
|
||||||
|
open MailKit.Security
|
||||||
|
open MimeKit
|
||||||
|
|
||||||
|
/// Send an account confirmation e-mail
|
||||||
|
let sendAccountConfirmation citizen security = backgroundTask {
|
||||||
|
let name = Citizen.name citizen
|
||||||
|
let token = security.Token.Value
|
||||||
|
use client = new SmtpClient ()
|
||||||
|
do! client.ConnectAsync ("localhost", 25, SecureSocketOptions.None)
|
||||||
|
|
||||||
|
use msg = new MimeMessage ()
|
||||||
|
msg.From.Add (MailboxAddress ("Jobs, Jobs, Jobs", "daniel@bitbadger.solutions" (* "summersd@localhost" *) ))
|
||||||
|
msg.To.Add (MailboxAddress (name, citizen.Email (* "summersd@localhost" *) ))
|
||||||
|
msg.Subject <- "Account Confirmation Request"
|
||||||
|
|
||||||
|
let text =
|
||||||
|
[ $"ITM, {name}!"
|
||||||
|
""
|
||||||
|
"This e-mail address was recently used to establish an account on"
|
||||||
|
"Jobs, Jobs, Jobs (noagendacareers.com). Before this account can be"
|
||||||
|
"used, it needs to be verified. Please click the link below to do so;"
|
||||||
|
"it will work for the next 72 hours (3 days)."
|
||||||
|
""
|
||||||
|
$"https://noagendacareers.com/citizen/confirm/{token}"
|
||||||
|
""
|
||||||
|
"If you did not take this action, you can do nothing, and the account"
|
||||||
|
"will be deleted at the end of that time. If you wish to delete it"
|
||||||
|
"immediately, use the link below (also valid for 72 hours)."
|
||||||
|
""
|
||||||
|
$"https://noagendacareers.com/citizen/deny/{token}"
|
||||||
|
""
|
||||||
|
"TYFYC!"
|
||||||
|
""
|
||||||
|
"--"
|
||||||
|
"Jobs, Jobs, Jobs"
|
||||||
|
"https://noagendacareers.com"
|
||||||
|
] |> String.concat "\n"
|
||||||
|
use msgText = new TextPart (Text = text)
|
||||||
|
msg.Body <- msgText
|
||||||
|
|
||||||
|
return! client.SendAsync msg
|
||||||
|
}
|
|
@ -132,14 +132,24 @@ module Citizen =
|
||||||
TokenExpires = Some (now + (Duration.FromDays 3))
|
TokenExpires = Some (now + (Duration.FromDays 3))
|
||||||
}
|
}
|
||||||
do! Citizens.register citizen security
|
do! Citizens.register citizen security
|
||||||
// TODO: generate auth code and e-mail confirmation
|
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
|
return! ok next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: /api/citizen/confirm
|
// PATCH: /api/citizen/confirm
|
||||||
let confirmToken : HttpHandler = fun next ctx -> task {
|
let confirmToken : HttpHandler = fun next ctx -> task {
|
||||||
let! form = ctx.BindJsonAsync<{| token : string |}> ()
|
let! form = ctx.BindJsonAsync<{| token : string |}> ()
|
||||||
let! valid = Citizens.confirmAccount form.token (now ctx)
|
let! valid = Citizens.confirmAccount form.token
|
||||||
|
return! json {| valid = valid |} next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: /api/citizen/deny
|
||||||
|
let denyToken : HttpHandler = fun next ctx -> task {
|
||||||
|
let! form = ctx.BindJsonAsync<{| token : string |}> ()
|
||||||
|
let! valid = Citizens.denyAccount form.token
|
||||||
return! json {| valid = valid |} next ctx
|
return! json {| valid = valid |} next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,7 +516,10 @@ let allEndpoints = [
|
||||||
]
|
]
|
||||||
PATCH [ route "/confirm" Citizen.confirmToken ]
|
PATCH [ route "/confirm" Citizen.confirmToken ]
|
||||||
POST [ route "/register" Citizen.register ]
|
POST [ route "/register" Citizen.register ]
|
||||||
DELETE [ route "" Citizen.delete ]
|
DELETE [
|
||||||
|
route "" Citizen.delete
|
||||||
|
route "/deny" Citizen.denyToken
|
||||||
|
]
|
||||||
]
|
]
|
||||||
GET_HEAD [ route "/continents" Continent.all ]
|
GET_HEAD [ route "/continents" Continent.all ]
|
||||||
GET_HEAD [ route "/instances" Instances.all ]
|
GET_HEAD [ route "/instances" Instances.all ]
|
||||||
|
|
|
@ -9,13 +9,14 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Auth.fs" />
|
<Compile Include="Auth.fs" />
|
||||||
|
<Compile Include="Email.fs" />
|
||||||
<Compile Include="Handlers.fs" />
|
<Compile Include="Handlers.fs" />
|
||||||
<Compile Include="App.fs" />
|
<Compile Include="App.fs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Data\JobsJobsJobs.Data.fsproj" />
|
||||||
<ProjectReference Include="..\Domain\JobsJobsJobs.Domain.fsproj" />
|
<ProjectReference Include="..\Domain\JobsJobsJobs.Domain.fsproj" />
|
||||||
<ProjectReference Include="..\JobsJobsJobs.Data\JobsJobsJobs.Data.fsproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
<PackageReference Include="Giraffe" Version="6.0.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="6.0.8" />
|
||||||
<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" />
|
||||||
|
|
Loading…
Reference in New Issue
Block a user