Multi instance #26

Merged
danieljsummers merged 4 commits from multi-instance into main 2021-09-07 01:20:51 +00:00
19 changed files with 168 additions and 76 deletions
Showing only changes of commit 989beb0659 - Show all commits

View File

@ -6,7 +6,7 @@ open System.Text.Json.Serialization
/// The variables we need from the account information we get from Mastodon
[<NoComparison; NoEquality; AllowNullLiteral>]
type MastodonAccount () =
/// The user name (what we store as naUser)
/// The user name (what we store as mastodonUser)
[<JsonPropertyName "username">]
member val Username = "" with get, set
/// The account name; will generally be the same as username for local accounts, which is all we can verify

View File

@ -6,6 +6,7 @@ open JobsJobsJobs.Domain.Types
open Polly
open RethinkDb.Driver
open RethinkDb.Driver.Net
open RethinkDb.Driver.Ast
/// Shorthand for the RethinkDB R variable (how every command starts)
let private r = RethinkDB.R
@ -166,10 +167,20 @@ module Startup =
log.LogInformation $"Creating \"{idx}\" index on {table}"
r.Table(table).IndexCreate(idx).RunWriteAsync conn |> awaitIgnore)
}
do! ensureIndexes Table.Citizen [ "naUser" ]
do! ensureIndexes Table.Listing [ "citizenId"; "continentId"; "isExpired" ]
do! ensureIndexes Table.Profile [ "continentId" ]
do! ensureIndexes Table.Success [ "citizenId" ]
// The instance/user is a compound index
let! userIdx = r.Table(Table.Citizen).IndexList().RunResultAsync<string list> conn
match userIdx |> List.contains "instanceUser" with
| true -> ()
| false ->
let! _ =
r.Table(Table.Citizen)
.IndexCreate("instanceUser",
ReqlFunction1 (fun row -> upcast r.Array (row.G "instance", row.G "mastodonUser")))
.RunWriteAsync conn
()
}
@ -215,7 +226,6 @@ let regexContains = System.Text.RegularExpressions.Regex.Escape >> sprintf "(?i)
open JobsJobsJobs.Domain
open JobsJobsJobs.Domain.SharedTypes
open RethinkDb.Driver.Ast
/// Profile data access functions
[<RequireQualifiedAccess>]
@ -287,7 +297,7 @@ module Profile =
.HashMap("displayName",
r.Branch (it.G("realName" ).Default_("").Ne "", it.G "realName",
it.G("displayName").Default_("").Ne "", it.G "displayName",
it.G "naUser"))
it.G "mastodonUser"))
.With ("citizenId", it.G "id")))
.Pluck("citizenId", "displayName", "seekingEmployment", "remoteWork", "fullTime", "lastUpdatedOn")
.OrderBy(ReqlFunction1 (fun it -> upcast it.G("displayName").Downcase ()))
@ -348,10 +358,10 @@ module Citizen =
.RunResultAsync<Citizen>
|> withReconnOption conn
/// Find a citizen by their No Agenda Social username
let findByNaUser (naUser : string) conn =
/// Find a citizen by their Mastodon username
let findByMastodonUser (instance : string) (mastodonUser : string) conn =
r.Table(Table.Citizen)
.GetAll(naUser).OptArg("index", "naUser").Nth(0)
.GetAll(r.Array (instance, mastodonUser)).OptArg("index", "instanceUser").Nth(0)
.RunResultAsync<Citizen>
|> withReconnOption conn
@ -546,9 +556,17 @@ module Success =
.HashMap("citizenName",
r.Branch(it.G("realName" ).Default_("").Ne "", it.G "realName",
it.G("displayName").Default_("").Ne "", it.G "displayName",
it.G "naUser"))
it.G "mastodonUser"))
.With ("hasStory", it.G("story").Default_("").Gt "")))
.Pluck("id", "citizenId", "citizenName", "recordedOn", "fromHere", "hasStory")
.OrderBy(r.Desc "recordedOn")
.RunResultAsync<StoryEntry list>
|> withReconn conn
(*
-- To replace naUser with mastodonUser for NAS --
r.db('jobsjobsjobs').table('citizen').replace(function(it) {
return it.merge({ mastodonUser: it('naUser'), instance: 'nas' }).without('naUser');
})
*)

View File

@ -117,22 +117,23 @@ module Citizen =
| Some instance ->
let log = (logger ctx).CreateLogger (nameof JobsJobsJobs.Api.Auth)
match! Auth.verifyWithMastodon authCode instance cfg.ReturnUrl log with
match! Auth.verifyWithMastodon authCode instance cfg.ReturnHost log with
| Ok account ->
// Step 2 - Find / establish Jobs, Jobs, Jobs account
let now = (clock ctx).GetCurrentInstant ()
let dbConn = conn ctx
let! citizen = task {
match! Data.Citizen.findByNaUser account.Username dbConn with
match! Data.Citizen.findByMastodonUser instance.Abbr account.Username dbConn with
| None ->
let it : Citizen =
{ id = CitizenId.create ()
naUser = account.Username
displayName = noneIfEmpty account.DisplayName
realName = None
profileUrl = account.Url
joinedOn = now
lastSeenOn = now
{ id = CitizenId.create ()
instance = instance.Abbr
mastodonUser = account.Username
displayName = noneIfEmpty account.DisplayName
realName = None
profileUrl = account.Url
joinedOn = now
lastSeenOn = now
}
do! Data.Citizen.add it dbConn
return it

View File

@ -1,5 +1,7 @@
{
"Rethink": {
"Hostname": "data02.bitbadger.solutions",
"Db": "jobsjobsjobs-dev"
},
"Auth": {
"ReturnHost": "http://localhost:5000",

View File

@ -46,7 +46,7 @@ export function yesOrNo (cond : boolean) : string {
* @returns The citizen's display name
*/
export function citizenName (cit : Citizen) : string {
return cit.realName ?? cit.displayName ?? cit.naUser
return cit.realName ?? cit.displayName ?? cit.mastodonUser
}
</script>

View File

@ -3,8 +3,10 @@
export interface Citizen {
/** The ID of the user */
id : string
/** The abbreviation of the instance where this citizen is based */
instance : string
/** The handle by which the user is known on Mastodon */
naUser : string
mastodonUser : string
/** The user's display name from Mastodon (updated every login) */
displayName : string | undefined
/** The user's real name */

View File

@ -121,7 +121,7 @@ const routes: Array<RouteRecordRaw> = [
component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionOptions.vue")
},
{
path: "/so-long/success",
path: "/so-long/success/:url",
name: "DeletionSuccess",
component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionSuccess.vue")
},

View File

@ -24,7 +24,7 @@ export default createStore({
state: () : State => {
return {
user: undefined,
logOnState: "<em>Welcome back! Verifying your No Agenda Social account&hellip;</em>",
logOnState: "<em>Welcome back!</em>",
continents: []
}
},

View File

@ -21,8 +21,8 @@ article
p.
Clicking the #[span.link View] link on a listing brings up the full view page for a listing. This page displays all
of the information from the search results, along with the citizen who posted it, and the full details of the job.
The citizen&rsquo;s name is a link to their profile page at No Agenda Social; you can use that to get their handle,
and use NAS&rsquo;s communication facilites to inquire about the position.
The citizen&rsquo;s name is a link to their profile page at their Mastodon instance; you can use that to get their
handle, and use Mastodon&rsquo;s communication facilites to inquire about the position.
p: em.text-muted.
(If you know of a way to construct a link to Mastodon that would start a direct message, please reach out;
I&rsquo;ve searched and searched, and asked NAS, but have not yet determined how to do that.)
@ -43,9 +43,9 @@ article
The #[span.link My Job Listings] page will show you all of your active job listings just below the
#[span.button Add a Job Listing] button. Within this table, you can edit the listing, view it, or expire it (more on
that below). The #[span.link View] link will show you the job listing just as other users will see it. You can share
the link from your browser over on No Agenda Social, and those who click on it will be able to view it. (Existing
users of Jobs, Jobs, Jobs will go right to it; others will need to authorize this site&rsquo;s access, but then they
will get there as well.)
the link from your browser on any No Agenda-affiliated Mastodon instance, and those who click on it will be able to
view it. (Existing users of Jobs, Jobs, Jobs will go right to it; others will need to authorize this site&rsquo;s
access, but then they will get there as well.)
h5 Expire a Job Listing
p.
@ -68,7 +68,7 @@ article
The #[span.link Employment Profiles] link at the side allows you to search for profiles by continent, the
citizen&rsquo;s desire for remote work, a skill, or any text in their professional biography and experience. If you
find someone with whom you&rsquo;d like to discuss potential opportunities, the name at the top of the profile links
to their No Agenda Social account, where you can use its features to get in touch.
to their Mastodon profile, where you can use its features to get in touch.
hr
@ -76,8 +76,8 @@ article
p.
The employment profile is your r&eacute;sum&eacute;, visible to other citizens here. It also allows you to specify
your real name, if you so desire; if that is filled in, that is how you will be identified in search results,
profile views, etc. If not, you will be identified as you are on No Agenda Social; this system updates your current
display name each time you log on.
profile views, etc. If not, you will be identified as you are on your Mastodon instance; this system updates your
current display name each time you log on.
h5 Completing Your Profile
p.
@ -99,19 +99,19 @@ article
li.
If you check the #[span.link Allow my profile to be searched publicly] checkbox #[strong and] you are seeking
employment, your continent, region, and skills fields will be searchable and displayed to public users of the
site. They will not be tied to your No Agenda Social handle or real name; they are there to let people peek
behind the curtain a bit, and hopefully inspire them to join us.
site. They will not be tied to your Mastodon handle or real name; they are there to let people peek behind the
curtain a bit, and hopefully inspire them to join us.
h5 Viewing and Sharing Your Profile
p.
Once your profile has been established, the #[span.link My Employment Profile] page will have a button at the bottom
that will let you view your profile the way all other validated users will be able to see it. (There will also be a
link to this page from the #[span.link Dashboard].) The URL of this page can be shared on No Agenda Social, if you
would like to share it there. Just as with job listings, existing users will go straight there, while other No
Agenda Social users will get there once they authorize this application.
link to this page from the #[span.link Dashboard].) The URL of this page can be shared on any No Agenda-affiliated
Mastodon instance, if you would like to share it there. Just as with job listings, existing users will go straight
there, while others will get there once they authorize this application.
p.
The name on employment profiles is a link to that user&rsquo;s profile on No Agenda Social; from there, others can
communicate further with you using the tools Mastodon provides.
The name on employment profiles is a link to that user&rsquo;s profile on their Mastodon instance; from there,
others can communicate further with you using the tools Mastodon provides.
h5 &ldquo;I Found a Job!&rdquo;
p.

View File

@ -2,7 +2,7 @@
article
page-title(title="Privacy Policy")
h3 Privacy Policy
p: em (as of February 6#[sup th], 2021)
p: em (as of September 6#[sup th], 2021)
p.
{{name}} (&ldquo;we,&rdquo; &ldquo;our,&rdquo; or &ldquo;us&rdquo;) is committed to protecting your privacy. This
@ -58,7 +58,7 @@ article
li Name / Username
li Coarse Geographic Location
li Employment History
li No Agenda Social Account Name / Profile
li Mastodon Account Name / Profile
h4 How Do We Use The Information We Collect?
p Any of the information we collect from you may be used in one of the following ways:
@ -75,9 +75,9 @@ article
p {{name}} will collect End User Data necessary to provide the {{name}} services to our customers.
p.
End users may voluntarily provide us with information they have made available on social media websites
(specifically No Agenda Social). If you provide us with any such information, we may collect publicly available
information from the social media websites you have indicated. You can control how much of your information social
media websites make public by visiting these websites and changing your privacy settings.
(specifically No Agenda-affiliated Mastodon instances). If you provide us with any such information, we may collect
publicly available information from the social media websites you have indicated. You can control how much of your
information social media websites make public by visiting these websites and changing your privacy settings.
h4 When does {{name}} use customer information from third parties?
p We do not utilize third party information apart from the end-user data described above.
@ -223,10 +223,10 @@ article
h4 Tracking Technologies
p.
{{name}} does not use any tracking technologies. When an authorization code is received from No Agenda Social, that
token is stored in the browser&rsquo;s memory, and the Service uses tokens on each request for data. If the page is
refreshed or the browser window/tab is closed, this token disappears, and a new one must be generated before the
application can be used again.
{{name}} does not use any tracking technologies. When an authorization code is received from Mastodon, that token is
stored in the browser&rsquo;s memory, and the Service uses tokens on each request for data. If the page is refreshed
or the browser window/tab is closed, this token disappears, and a new one must be generated before the application
can be used again.
h4 Information about General Data Protection Regulation (GDPR)
p.
@ -335,6 +335,12 @@ article
h4 Contact Us
p Don&rsquo;t hesitate to contact us if you have any questions.
ul: li Via this Link: #[router-link(to="/how-it-works") https://noagendacareers.com/how-it-works]
hr
p: em.
Change on September 6#[sup th], 2021 &ndash; replaced &ldquo;No Agenda Social&rdquo; with generic terms for any
authorized Mastodon instance.
</template>
<script setup lang="ts">

View File

@ -2,7 +2,7 @@
article
page-title(title="Terms of Service")
h3 Terms of Service
p: em (as of February 6#[sup th], 2021)
p: em (as of September 6#[sup th], 2021)
h4 Acceptance of Terms
p.
@ -11,12 +11,20 @@ article
acceptance of these terms.
h4 Description of Service and Registration
p.
Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles, restricting access
to the details of these profiles to other users of
#[a(href="https://noagendasocial.com" target="_blank") No Agenda Social]. Registration is accomplished by allowing
Jobs, Jobs, Jobs to read one&rsquo;s No Agenda Social profile. See our
#[router-link(to="/privacy-policy") privacy policy] for details on the personal (user) information we maintain.
p
| Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles, restricting access
| to the details of these profiles to other users of
= " "
template(v-for="(it, idx) in instances" :key="idx")
a(:href="it.url" target="_blank") {{it.name}}
template(v-if="idx + 2 < instances.length")= ", "
template(v-else-if="idx + 1 < instances.length")= ", and "
template(v-else)= ". "
| Registration is accomplished by allowing Jobs, Jobs, Jobs to read one&rsquo;s Mastodon profile. See our
= " "
router-link(to="/privacy-policy") privacy policy
= " "
| for details on the personal (user) information we maintain.
h4 Liability
p.
@ -34,4 +42,30 @@ article
p.
You may also wish to review our #[router-link(to="/privacy-policy") privacy policy] to learn how we handle your
data.
hr
p: em.
Change on September 6#[sup th], 2021 &ndash; replaced &ldquo;No Agenda Social&rdquo; with a list of all No
Agenda-affiliated Mastodon instances.
</template>
<script setup lang="ts">
import { onMounted, Ref, ref } from "vue"
import api, { Instance } from "@/api"
import { toastError } from "@/components/layout/AppToaster.vue"
const instances : Ref<Instance[]> = ref([])
onMounted(async () => {
const apiResp = await api.instances.all()
if (typeof apiResp === "string") {
toastError(apiResp, "retrieving instances")
} else if (typeof apiResp === "undefined") {
toastError("No instances to display", undefined)
} else {
instances.value = apiResp
}
})
</script>

View File

@ -30,6 +30,7 @@ const logOn = async () => {
} else if (typeof instance === "undefined") {
setMessage(`Mastodon instance ${abbr} not found`)
} else {
setMessage(`<em>Welcome back! Verifying your ${instance.name} account&hellip;</em>`)
const code = route.query.code
if (code) {
await store.dispatch("logOn", { abbr, code })

View File

@ -24,7 +24,6 @@ const selected : Ref<Instance | undefined> = ref(undefined)
/** The authorization URL to which the user should be directed */
const authUrl = computed(() => {
if (selected.value) {
/** The client ID for Jobs, Jobs, Jobs at No Agenda Social */
const client = `client_id=${selected.value.clientId}`
const scope = "scope=read:accounts"
const redirect = `redirect_uri=${document.location.origin}/citizen/${selected.value.abbr}/authorized`
@ -35,7 +34,7 @@ const authUrl = computed(() => {
})
/**
* Select a given Mastadon instance
* Select a given Mastodon instance
*
* @param abbr The abbreviation of the instance being selected
*/

View File

@ -66,7 +66,7 @@ const title = computed(() => it.value ? `${it.value.listing.title} | Job Listing
const details = computed(() => toHtml(it.value?.listing.text ?? ""))
/** The NAS profile URL for the citizen who posted this job listing */
const profileUrl = computed(() => citizen.value ? `https://noagendasocial.com/@${citizen.value.naUser}` : "")
const profileUrl = computed(() => citizen.value ? citizen.value.profileUrl : "")
/** The needed by date, formatted in SHOUTING MODE */
const neededBy = (nb : string) => formatNeededBy(nb).toUpperCase()

View File

@ -13,10 +13,10 @@ article
p.
This option will make it like you never visited this site. It will delete your profile, skills, success stories, and
account. This is what you want to use if you want to disappear from this application. Clicking the button below
#[strong will not] affect your No Agenda Social account in any way; its effects are limited to Jobs, Jobs, Jobs.
#[strong will not] affect your Mastodon account in any way; its effects are limited to Jobs, Jobs, Jobs.
p: em.
(This will not revoke this application&rsquo;s permissions on No Agenda Social; you will have to remove this
yourself. The confirmation message has a link where you can do this; once the page loads, find the
(This will not revoke this application&rsquo;s permissions on Mastodon; you will have to remove this yourself. The
confirmation message has a link where you can do this; once the page loads, find the
#[strong Jobs, Jobs, Jobs] entry, and click the #[strong &times; Revoke] link for that entry.)
p.text-center: button.btn.btn-danger(@click.prevent="deleteAccount") Delete Your Entire Account
</template>
@ -32,9 +32,12 @@ import { useStore } from "@/store"
const store = useStore()
const router = useRouter()
/** The currently logged-on user */
const user = store.state.user as LogOnSuccess
/** Delete the profile only; redirect to home page on success */
const deleteProfile = async () => {
const resp = await api.profile.delete(store.state.user as LogOnSuccess)
const resp = await api.profile.delete(user)
if (typeof resp === "string") {
toastError(resp, "Deleting Profile")
} else {
@ -45,13 +48,27 @@ const deleteProfile = async () => {
/** Delete everything pertaining to the user's account */
const deleteAccount = async () => {
const resp = await api.citizen.delete(store.state.user as LogOnSuccess)
if (typeof resp === "string") {
toastError(resp, "Deleting Account")
const citizenResp = await api.citizen.retrieve(user.citizenId, user)
if (typeof citizenResp === "string") {
toastError(citizenResp, "retrieving citizen")
} else if (typeof citizenResp === "undefined") {
toastError("Could not retrieve citizen record", undefined)
} else {
store.commit("clearUser")
toastSuccess("Account Deleted Successfully")
router.push("/so-long/success")
const instResp = await api.instances.byAbbr(citizenResp.instance)
if (typeof instResp === "string") {
toastError(instResp, "retriving instance")
} else if (typeof instResp === "undefined") {
toastError("Could not retrieve instance", undefined)
} else {
const resp = await api.citizen.delete(user)
if (typeof resp === "string") {
toastError(resp, "Deleting Account")
} else {
store.commit("clearUser")
toastSuccess("Account Deleted Successfully")
router.push(`/so-long/success/${instResp.url}`)
}
}
}
}
</script>

View File

@ -4,8 +4,18 @@ article
h3.pb-3 Account Deletion Success
p.
Your account has been successfully deleted. To revoke the permissions you have previously granted to this
application, find it in #[a(href="https://noagendasocial.com/oauth/authorized_applications") this list] and click
application, find it in #[a(:href="`${url}/oauth/authorized_applications`") this list] and click
#[strong &times; Revoke]. Otherwise, clicking &ldquo;Log On&rdquo; in the left-hand menu will create a new, empty
account without prompting you further.
p Thank you for participating, and thank you for your courage. #GitmoNation
</template>
<script setup lang="ts">
import { useRoute } from "vue-router"
const route = useRoute()
/** The URL of the instance from which the deleted user had authorized access */
const url = route.params.url as string
</script>

View File

@ -33,7 +33,7 @@ module CitizenId =
module Citizen =
/// Get the name of the citizen (the first of real name, display name, or handle that is filled in)
let name x =
[ x.realName; x.displayName; Some x.naUser ]
[ x.realName; x.displayName; Some x.mastodonUser ]
|> List.find Option.isSome
|> Option.get

View File

@ -92,8 +92,8 @@ type MastodonInstance () =
/// The authorization options for Jobs, Jobs, Jobs
type AuthOptions () =
/// The return URL for Mastodoon verification
member val ReturnUrl = "" with get, set
/// The host for the return URL for Mastodoon verification
member val ReturnHost = "" with get, set
/// The secret with which the server signs the JWTs for auth once we've verified with Mastodon
member val ServerSecret = "" with get, set
/// The instances configured for use

View File

@ -13,19 +13,21 @@ type CitizenId = CitizenId of Guid
[<CLIMutable; NoComparison; NoEquality>]
type Citizen = {
/// The ID of the user
id : CitizenId
id : CitizenId
/// The Mastodon instance abbreviation from which this citizen is authorized
instance : string
/// The handle by which the user is known on Mastodon
naUser : string
mastodonUser : string
/// The user's display name from Mastodon (updated every login)
displayName : string option
displayName : string option
/// The user's real name
realName : string option
realName : string option
/// The URL for the user's Mastodon profile
profileUrl : string
profileUrl : string
/// When the user joined Jobs, Jobs, Jobs
joinedOn : Instant
joinedOn : Instant
/// When the user last logged in
lastSeenOn : Instant
lastSeenOn : Instant
}