Remove Vue / F# files
This commit is contained in:
parent
1f19933c23
commit
ea8edb2937
3
src/JobsJobsJobs.Api/.gitignore
vendored
3
src/JobsJobsJobs.Api/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
|||
obj/
|
||||
bin/
|
||||
appsettings.*.json
|
|
@ -1,59 +0,0 @@
|
|||
module JobsJobsJobs.Api.Auth
|
||||
|
||||
open Data
|
||||
open Domain
|
||||
open FSharp.Json
|
||||
open JWT.Algorithms
|
||||
open JWT.Builder
|
||||
open JWT.Exceptions
|
||||
open System
|
||||
open System.Net.Http
|
||||
open System.Net.Http.Headers
|
||||
|
||||
/// Verify a user's credentials with No Agenda Social
|
||||
let verifyWithMastodon accessToken = async {
|
||||
use client = new HttpClient ()
|
||||
use req = new HttpRequestMessage (HttpMethod.Get, $"{config.auth.apiUrl}accounts/verify_credentials")
|
||||
req.Headers.Authorization <- AuthenticationHeaderValue ("Bearer", accessToken)
|
||||
match! client.SendAsync req |> Async.AwaitTask with
|
||||
| res when res.IsSuccessStatusCode ->
|
||||
let! body = res.Content.ReadAsStringAsync ()
|
||||
return
|
||||
match Json.deserialize<ViewModels.Citizen.MastodonAccount> body with
|
||||
| profile when profile.username = profile.acct -> Ok profile
|
||||
| profile -> Error $"Profiles must be from noagendasocial.com; yours is {profile.acct}"
|
||||
| res -> return Error $"Could not retrieve credentials: %d{int res.StatusCode} ~ {res.ReasonPhrase}"
|
||||
}
|
||||
|
||||
/// Create a JWT for the given user
|
||||
let createJwt citizenId = async {
|
||||
match! Citizens.tryFind citizenId with
|
||||
| Ok (Some citizen) ->
|
||||
return
|
||||
JwtBuilder()
|
||||
.WithAlgorithm(HMACSHA256Algorithm ())
|
||||
// TODO: generate separate secret for server
|
||||
.WithSecret(config.auth.secret)
|
||||
.AddClaim("sub", CitizenId.toString citizen.id)
|
||||
.AddClaim("exp", DateTimeOffset.UtcNow.AddHours(1.).ToUnixTimeSeconds ())
|
||||
.AddClaim("nam", citizen.displayName)
|
||||
.Encode ()
|
||||
|> Ok
|
||||
| Ok None -> return Error (exn "Citizen record not found")
|
||||
| Error exn -> return Error exn
|
||||
}
|
||||
|
||||
/// Validate the given token
|
||||
let validateJwt token =
|
||||
try
|
||||
let paylod =
|
||||
JwtBuilder()
|
||||
.WithAlgorithm(HMACSHA256Algorithm ())
|
||||
// TODO: generate separate secret for server
|
||||
.WithSecret(config.auth.secret)
|
||||
.MustVerifySignature()
|
||||
.Decode<Map<string, obj>> token
|
||||
CitizenId.tryParse (paylod.["sub"] :?> string)
|
||||
with
|
||||
| :? TokenExpiredException -> Error "Token is expired"
|
||||
| :? SignatureVerificationException -> Error "Invalid token signature"
|
|
@ -1,134 +0,0 @@
|
|||
module JobsJobsJobs.Api.Data
|
||||
|
||||
open JobsJobsJobs.Api.Domain
|
||||
open Npgsql.FSharp
|
||||
open System
|
||||
|
||||
/// The connection URI for the database
|
||||
let connectUri = Uri config.dbUri
|
||||
|
||||
|
||||
/// Connect to the database
|
||||
let db () =
|
||||
(Sql.fromUri >> Sql.connect) connectUri
|
||||
|
||||
|
||||
/// Return None if the error is that a single row was expected, but no rows matched
|
||||
let private noneIfNotFound (it : Async<Result<'T, exn>>) = async {
|
||||
match! it with
|
||||
| Ok x ->
|
||||
return (Some >> Ok) x
|
||||
| Error err ->
|
||||
return match err.Message with msg when msg.Contains "at least one" -> Ok None | _ -> Error err
|
||||
}
|
||||
|
||||
|
||||
/// Get the item count from a single-row result
|
||||
let private itemCount (read: RowReader) = read.int64 "item_count"
|
||||
|
||||
|
||||
/// Functions for manipulating citizens
|
||||
// (SHUT UP, SLAVE!)
|
||||
module Citizens =
|
||||
|
||||
/// Create a Citizen from a row of data
|
||||
let private fromReader (read: RowReader) =
|
||||
match (read.string >> CitizenId.tryParse) "id" with
|
||||
| Ok citizenId -> {
|
||||
id = citizenId
|
||||
naUser = read.string "na_user"
|
||||
displayName = read.string "display_name"
|
||||
profileUrl = read.string "profile_url"
|
||||
joinedOn = (read.int64 >> Millis) "joined_on"
|
||||
lastSeenOn = (read.int64 >> Millis) "last_seen_on"
|
||||
}
|
||||
| Error err -> failwith err
|
||||
|
||||
/// Determine if we already know about this user from No Agenda Social
|
||||
let findIdByNaUser naUser =
|
||||
db ()
|
||||
|> Sql.query "SELECT id FROM citizen WHERE na_user = @na_user"
|
||||
|> Sql.parameters [ "@na_user", Sql.string naUser ]
|
||||
|> Sql.executeRowAsync (fun read ->
|
||||
match (read.string >> CitizenId.tryParse) "id" with
|
||||
| Ok citizenId -> citizenId
|
||||
| Error err -> failwith err)
|
||||
|> noneIfNotFound
|
||||
|
||||
/// Add a citizen
|
||||
let add citizen =
|
||||
db ()
|
||||
|> Sql.query
|
||||
"""INSERT INTO citizen (
|
||||
na_user, display_name, profile_url, joined_on, last_seen_on, id
|
||||
) VALUES (
|
||||
@na_user, @display_name, @profile_url, @joined_on, @last_seen_on, @id
|
||||
)"""
|
||||
|> Sql.parameters [
|
||||
"@na_user", Sql.string citizen.naUser
|
||||
"@display_name", Sql.string citizen.displayName
|
||||
"@profile_url", Sql.string citizen.profileUrl
|
||||
"@joined_on", (Millis.toLong >> Sql.int64) citizen.joinedOn
|
||||
"@last_seen_on", (Millis.toLong >> Sql.int64) citizen.lastSeenOn
|
||||
"@id", (CitizenId.toString >> Sql.string) citizen.id
|
||||
]
|
||||
|> Sql.executeNonQueryAsync
|
||||
|
||||
/// Update a citizen record when they log on
|
||||
let update citizenId displayName =
|
||||
db ()
|
||||
|> Sql.query
|
||||
"""UPDATE citizen
|
||||
SET display_name = @display_name,
|
||||
last_seen_on = @last_seen_on
|
||||
WHERE id = @id"""
|
||||
|> Sql.parameters [
|
||||
"@display_name", Sql.string displayName
|
||||
"@last_seen_on", (DateTime.Now.toMillis >> Millis.toLong >> Sql.int64) ()
|
||||
"@id", (CitizenId.toString >> Sql.string) citizenId
|
||||
]
|
||||
|> Sql.executeNonQueryAsync
|
||||
|
||||
/// Try to find a citizen with the given ID
|
||||
let tryFind citizenId =
|
||||
db ()
|
||||
|> Sql.query "SELECT * FROM citizen WHERE id = @id"
|
||||
|> Sql.parameters [ "@id", (CitizenId.toString >> Sql.string) citizenId ]
|
||||
|> Sql.executeRowAsync fromReader
|
||||
|> noneIfNotFound
|
||||
|
||||
|
||||
/// Functions for manipulating employment profiles
|
||||
module Profiles =
|
||||
|
||||
/// Create a Profile from a row of data
|
||||
let private fromReader (read: RowReader) =
|
||||
match (read.string >> CitizenId.tryParse) "citizen_id" with
|
||||
| Ok citizenId ->
|
||||
match (read.string >> ContinentId.tryParse) "continent_id" with
|
||||
| Ok continentId -> {
|
||||
citizenId = citizenId
|
||||
seekingEmployment = read.bool "seeking_employment"
|
||||
isPublic = read.bool "is_public"
|
||||
continent = { id = continentId; name = read.string "continent_name" }
|
||||
region = read.string "region"
|
||||
remoteWork = read.bool "remote_work"
|
||||
fullTime = read.bool "full_time"
|
||||
biography = (read.string >> MarkdownString) "biography"
|
||||
lastUpdatedOn = (read.int64 >> Millis) "last_updated_on"
|
||||
experience = (read.stringOrNone >> Option.map MarkdownString) "experience"
|
||||
}
|
||||
| Error err -> failwith err
|
||||
| Error err -> failwith err
|
||||
|
||||
/// Try to find an employment profile for the given citizen ID
|
||||
let tryFind citizenId =
|
||||
db ()
|
||||
|> Sql.query
|
||||
"""SELECT p.*, c.name AS continent_name
|
||||
FROM profile p
|
||||
INNER JOIN continent c ON p.continent_id = c.id
|
||||
WHERE citizen_id = @id"""
|
||||
|> Sql.parameters [ "@id", (CitizenId.toString >> Sql.string) citizenId ]
|
||||
|> Sql.executeRowAsync fromReader
|
||||
|> noneIfNotFound
|
|
@ -1,272 +0,0 @@
|
|||
module JobsJobsJobs.Api.Domain
|
||||
|
||||
// fsharplint:disable RecordFieldNames MemberNames
|
||||
|
||||
/// A short ID (12 characters of a Nano ID)
|
||||
type ShortId =
|
||||
| ShortId of string
|
||||
|
||||
/// Functions to maniuplate short IDs
|
||||
module ShortId =
|
||||
|
||||
open Nanoid
|
||||
open System.Text.RegularExpressions
|
||||
|
||||
/// Regular expression to validate a string's format as a short ID
|
||||
let validShortId = Regex ("^[a-z0-9_-]{12}", RegexOptions.Compiled ||| RegexOptions.IgnoreCase)
|
||||
|
||||
/// Convert a short ID to its string representation
|
||||
let toString = function ShortId text -> text
|
||||
|
||||
/// Create a new short ID
|
||||
let create () = async {
|
||||
let! text = Nanoid.GenerateAsync (size = 12)
|
||||
return ShortId text
|
||||
}
|
||||
|
||||
/// Try to parse a string into a short ID
|
||||
let tryParse (text : string) =
|
||||
match text.Length with
|
||||
| 12 when validShortId.IsMatch text -> (ShortId >> Ok) text
|
||||
| 12 -> Error "ShortId must be 12 characters [a-z,0-9,-, or _]"
|
||||
| x -> Error $"ShortId must be 12 characters; %d{x} provided"
|
||||
|
||||
|
||||
/// The ID for a citizen (user) record
|
||||
type CitizenId =
|
||||
| CitizenId of ShortId
|
||||
|
||||
/// Functions for manipulating citizen (user) IDs
|
||||
module CitizenId =
|
||||
/// Convert a citizen ID to its string representation
|
||||
let toString = function CitizenId shortId -> ShortId.toString shortId
|
||||
|
||||
/// Create a new citizen ID
|
||||
let create () = async {
|
||||
let! shortId = ShortId.create ()
|
||||
return CitizenId shortId
|
||||
}
|
||||
|
||||
/// Try to parse a string into a CitizenId
|
||||
let tryParse text =
|
||||
match ShortId.tryParse text with
|
||||
| Ok shortId -> (CitizenId >> Ok) shortId
|
||||
| Error err -> Error err
|
||||
|
||||
|
||||
/// The ID for a continent record
|
||||
type ContinentId =
|
||||
| ContinentId of ShortId
|
||||
|
||||
/// Functions for manipulating continent IDs
|
||||
module ContinentId =
|
||||
/// Convert a continent ID to its string representation
|
||||
let toString = function ContinentId shortId -> ShortId.toString shortId
|
||||
|
||||
/// Create a new continent ID
|
||||
let create () = async {
|
||||
let! shortId = ShortId.create ()
|
||||
return ContinentId shortId
|
||||
}
|
||||
|
||||
/// Try to parse a string into a ContinentId
|
||||
let tryParse text =
|
||||
match ShortId.tryParse text with
|
||||
| Ok shortId -> (ContinentId >> Ok) shortId
|
||||
| Error err -> Error err
|
||||
|
||||
|
||||
/// The ID for a skill record
|
||||
type SkillId =
|
||||
| SkillId of ShortId
|
||||
|
||||
/// Functions for manipulating skill IDs
|
||||
module SkillId =
|
||||
/// Convert a skill ID to its string representation
|
||||
let toString = function SkillId shortId -> ShortId.toString shortId
|
||||
|
||||
/// Create a new skill ID
|
||||
let create () = async {
|
||||
let! shortId = ShortId.create ()
|
||||
return SkillId shortId
|
||||
}
|
||||
|
||||
/// Try to parse a string into a CitizenId
|
||||
let tryParse text =
|
||||
match ShortId.tryParse text with
|
||||
| Ok shortId -> (SkillId >> Ok) shortId
|
||||
| Error err -> Error err
|
||||
|
||||
|
||||
/// The ID for a success report record
|
||||
type SuccessId =
|
||||
| SuccessId of ShortId
|
||||
|
||||
/// Functions for manipulating success report IDs
|
||||
module SuccessId =
|
||||
/// Convert a success report ID to its string representation
|
||||
let toString = function SuccessId shortId -> ShortId.toString shortId
|
||||
|
||||
/// Create a new success report ID
|
||||
let create () = async {
|
||||
let! shortId = ShortId.create ()
|
||||
return SuccessId shortId
|
||||
}
|
||||
|
||||
/// Try to parse a string into a SuccessId
|
||||
let tryParse text =
|
||||
match ShortId.tryParse text with
|
||||
| Ok shortId -> (SuccessId >> Ok) shortId
|
||||
| Error err -> Error err
|
||||
|
||||
|
||||
/// A number representing milliseconds since the epoch (AKA JavaScript time)
|
||||
type Millis =
|
||||
| Millis of int64
|
||||
|
||||
/// Functions to manipulate ticks
|
||||
module Millis =
|
||||
/// Convert a Ticks instance to its primitive value
|
||||
let toLong = function Millis millis -> millis
|
||||
|
||||
|
||||
/// A string that holds Markdown-formatted text
|
||||
type MarkdownString =
|
||||
| MarkdownString of string
|
||||
|
||||
/// Functions to manipulate Markdown-formatted text
|
||||
module MarkdownString =
|
||||
|
||||
open Markdig
|
||||
|
||||
/// Markdown pipeline that supports all built-in Markdown extensions
|
||||
let private pipeline = MarkdownPipelineBuilder().UseAdvancedExtensions().Build ()
|
||||
|
||||
/// Get the plain-text (non-rendered) representation of the text
|
||||
let toText = function MarkdownString str -> str
|
||||
|
||||
/// Get the HTML (rendered) representation of the text
|
||||
let toHtml = function MarkdownString str -> Markdown.ToHtml (str, pipeline)
|
||||
|
||||
|
||||
|
||||
/// A user
|
||||
type Citizen = {
|
||||
/// The ID of the user
|
||||
id : CitizenId
|
||||
/// The user's handle on No Agenda Social
|
||||
naUser : string
|
||||
/// The user's display name from No Agenda Social (as of their last login here)
|
||||
displayName : string
|
||||
/// The URL to the user's profile on No Agenda Social
|
||||
profileUrl : string
|
||||
/// When the user signed up here
|
||||
joinedOn : Millis
|
||||
/// When the user last logged on here
|
||||
lastSeenOn : Millis
|
||||
}
|
||||
|
||||
|
||||
/// A continent
|
||||
type Continent = {
|
||||
/// The ID of the continent
|
||||
id : ContinentId
|
||||
/// The name of the continent
|
||||
name : string
|
||||
}
|
||||
|
||||
|
||||
/// An employment / skills profile
|
||||
type Profile = {
|
||||
/// The ID of the user to whom the profile applies
|
||||
citizenId : CitizenId
|
||||
/// Whether this user is actively seeking employment
|
||||
seekingEmployment : bool
|
||||
/// Whether information from this profile should appear in the public anonymous list of available skills
|
||||
isPublic : bool
|
||||
/// The continent on which the user is seeking employment
|
||||
continent : Continent
|
||||
/// The region within that continent where the user would prefer to work
|
||||
region : string
|
||||
/// Whether the user is looking for remote work
|
||||
remoteWork : bool
|
||||
/// Whether the user is looking for full-time work
|
||||
fullTime : bool
|
||||
/// The user's professional biography
|
||||
biography : MarkdownString
|
||||
/// When this profile was last updated
|
||||
lastUpdatedOn : Millis
|
||||
/// The user's experience
|
||||
experience : MarkdownString option
|
||||
}
|
||||
|
||||
|
||||
/// A skill which a user possesses
|
||||
type Skill = {
|
||||
/// The ID of the skill
|
||||
id : SkillId
|
||||
/// The ID of the user who possesses this skill
|
||||
citizenId : CitizenId
|
||||
/// The skill
|
||||
skill : string
|
||||
/// Notes about the skill (proficiency, experience, etc.)
|
||||
notes : string option
|
||||
}
|
||||
|
||||
|
||||
/// A success story
|
||||
type Success = {
|
||||
/// The ID of the success story
|
||||
id : SuccessId
|
||||
/// The ID of the user who experienced this success story
|
||||
citizenId : CitizenId
|
||||
/// When this story was recorded
|
||||
recordedOn : Millis
|
||||
/// Whether the success came from here; if Jobs, Jobs, Jobs led them to eventual employment
|
||||
fromHere : bool
|
||||
/// Their success story
|
||||
story : MarkdownString option
|
||||
}
|
||||
|
||||
|
||||
/// Configuration required for authentication with No Agenda Social
|
||||
type AuthConfig = {
|
||||
/// The client ID
|
||||
clientId : string
|
||||
/// The cryptographic secret
|
||||
secret : string
|
||||
/// The base URL for Mastodon's API access
|
||||
apiUrl : string
|
||||
}
|
||||
|
||||
/// Application configuration format
|
||||
type JobsJobsJobsConfig = {
|
||||
/// Auth0 configuration
|
||||
auth : AuthConfig
|
||||
/// Database connection URI
|
||||
dbUri : string
|
||||
}
|
||||
|
||||
|
||||
open Microsoft.Extensions.Configuration
|
||||
open System.IO
|
||||
|
||||
/// Configuration instance
|
||||
let config =
|
||||
(lazy
|
||||
(let root =
|
||||
ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory ())
|
||||
.AddJsonFile("appsettings.json")
|
||||
.AddJsonFile("appsettings.Development.json", true)
|
||||
.AddJsonFile("appsettings.Production.json", true)
|
||||
.AddEnvironmentVariables("JJJ_")
|
||||
.Build()
|
||||
let auth = root.GetSection "Auth"
|
||||
{ dbUri = root.["dbUri"]
|
||||
auth = {
|
||||
clientId = auth.["ClientId"]
|
||||
secret = auth.["Secret"]
|
||||
apiUrl = auth.["ApiUrl"]
|
||||
}
|
||||
})).Force()
|
|
@ -1,31 +0,0 @@
|
|||
[<AutoOpen>]
|
||||
module JobsJobsJobs.Api.Extensions
|
||||
|
||||
open System
|
||||
|
||||
// fsharplint:disable MemberNames
|
||||
|
||||
/// Extensions for the DateTime object
|
||||
type DateTime with
|
||||
|
||||
/// Constant for the ticks at the Unix epoch
|
||||
member __.UnixEpochTicks = (DateTime (1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).Ticks
|
||||
|
||||
/// Convert this DateTime to a JavaScript milliseconds-past-the-epoch value
|
||||
member this.toMillis () =
|
||||
(this.ToUniversalTime().Ticks - this.UnixEpochTicks) / 10000L |> Domain.Millis
|
||||
|
||||
|
||||
open FSharp.Json
|
||||
open Suave
|
||||
open System.Text
|
||||
|
||||
/// Extensions for Suave's context
|
||||
type HttpContext with
|
||||
|
||||
/// Deserialize an object from a JSON request body
|
||||
member this.fromJsonBody<'T> () =
|
||||
try
|
||||
Encoding.UTF8.GetString this.request.rawForm |> Json.deserialize<'T> |> Ok
|
||||
with x ->
|
||||
Error x
|
|
@ -1,162 +0,0 @@
|
|||
module JobsJobsJobs.Api.Handlers
|
||||
|
||||
open Data
|
||||
open Domain
|
||||
open FSharp.Json
|
||||
open Suave
|
||||
open Suave.Operators
|
||||
open System
|
||||
|
||||
[<AutoOpen>]
|
||||
module private Internal =
|
||||
|
||||
open Suave.Writers
|
||||
|
||||
/// Read the JWT and get the authorized user ID
|
||||
let authorizedUser : WebPart =
|
||||
fun ctx ->
|
||||
match ctx.request.header "Authorization" with
|
||||
| Choice1Of2 bearer ->
|
||||
let token = (bearer.Split " ").[1]
|
||||
match Auth.validateJwt token with
|
||||
| Ok citizenId ->
|
||||
setUserData "citizenId" citizenId ctx
|
||||
| Error err ->
|
||||
RequestErrors.BAD_REQUEST err ctx
|
||||
| Choice2Of2 _ ->
|
||||
RequestErrors.BAD_REQUEST "Authorization header must be specified" ctx
|
||||
|
||||
/// Send a JSON response
|
||||
let json x =
|
||||
Successful.OK (Json.serialize x)
|
||||
>=> setMimeType "application/json; charset=utf-8"
|
||||
|
||||
/// Get the current citizen ID from the context
|
||||
let currentCitizenId ctx =
|
||||
ctx.userState.["citizenId"] :?> CitizenId
|
||||
|
||||
|
||||
/// Handler to return the Vue application
|
||||
module Vue =
|
||||
|
||||
/// The application index page
|
||||
let app = Files.file "wwwroot/index.html"
|
||||
|
||||
|
||||
/// Handlers for error conditions
|
||||
module Error =
|
||||
|
||||
open Suave.Logging
|
||||
open Suave.Logging.Message
|
||||
|
||||
/// Handle errors
|
||||
let error (ex : Exception) msg =
|
||||
fun ctx ->
|
||||
seq {
|
||||
yield string ctx.request.url
|
||||
match msg with
|
||||
| "" -> ()
|
||||
| _ -> yield " ~ "; yield msg
|
||||
yield "\n"; yield (ex.GetType().Name); yield ": "; yield ex.Message; yield "\n"
|
||||
yield ex.StackTrace
|
||||
}
|
||||
|> Seq.reduce (+)
|
||||
|> (eventX >> ctx.runtime.logger.error)
|
||||
ServerErrors.INTERNAL_ERROR (Json.serialize {| error = ex.Message |}) ctx
|
||||
|
||||
/// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there
|
||||
let notFound : WebPart =
|
||||
fun ctx ->
|
||||
[ "/user"; "/jobs" ]
|
||||
|> List.filter ctx.request.path.StartsWith
|
||||
|> List.length
|
||||
|> function
|
||||
| 0 -> RequestErrors.NOT_FOUND "err" ctx
|
||||
| _ -> Vue.app ctx
|
||||
|
||||
|
||||
/// /api/citizen route handlers
|
||||
module Citizen =
|
||||
|
||||
open ViewModels.Citizen
|
||||
|
||||
/// Either add the user, or update their display name and last seen date
|
||||
let establishCitizen result profile = async {
|
||||
match result with
|
||||
| Some citId ->
|
||||
match! Citizens.update citId profile.displayName with
|
||||
| Ok _ -> return Ok citId
|
||||
| Error exn -> return Error exn
|
||||
| None ->
|
||||
let now = DateTime.Now.toMillis ()
|
||||
let! citId = CitizenId.create ()
|
||||
match! Citizens.add
|
||||
{ id = citId
|
||||
naUser = profile.username
|
||||
displayName = profile.displayName
|
||||
profileUrl = profile.url
|
||||
joinedOn = now
|
||||
lastSeenOn = now
|
||||
} with
|
||||
| Ok _ -> return Ok citId
|
||||
| Error exn -> return Error exn
|
||||
}
|
||||
|
||||
/// POST: /api/citizen/log-on
|
||||
let logOn : WebPart =
|
||||
fun ctx -> async {
|
||||
match ctx.fromJsonBody<LogOn> () with
|
||||
| Ok data ->
|
||||
match! Auth.verifyWithMastodon data.accessToken with
|
||||
| Ok profile ->
|
||||
match! Citizens.findIdByNaUser profile.username with
|
||||
| Ok idResult ->
|
||||
match! establishCitizen idResult profile with
|
||||
| Ok citizenId ->
|
||||
match! Auth.createJwt citizenId with
|
||||
| Ok jwt -> return! json {| accessToken = jwt |} ctx
|
||||
| Error exn -> return! Error.error exn "Could not issue access token" ctx
|
||||
| Error exn -> return! Error.error exn "Could not update Jobs, Jobs, Jobs database" ctx
|
||||
| Error exn -> return! Error.error exn "Token not received" ctx
|
||||
| Error msg -> return! Error.error (exn msg) "Could not authenticate with NAS" ctx
|
||||
| Error exn -> return! Error.error exn "Token not received" ctx
|
||||
}
|
||||
|
||||
|
||||
/// /api/profile route handlers
|
||||
module Profile =
|
||||
|
||||
/// GET: /api/profile
|
||||
let get citizenId : WebPart =
|
||||
authorizedUser
|
||||
>=> fun ctx -> async {
|
||||
match (match citizenId with "" -> Ok (currentCitizenId ctx) | _ -> CitizenId.tryParse citizenId) with
|
||||
| Ok citId ->
|
||||
match! Profiles.tryFind citId with
|
||||
| Ok (Some profile) -> return! json profile ctx
|
||||
| Ok None -> return! Successful.NO_CONTENT ctx
|
||||
| Error exn -> return! Error.error exn "Cannot retrieve profile" ctx
|
||||
| Error _ -> return! Error.notFound ctx
|
||||
}
|
||||
|
||||
|
||||
open Suave.Filters
|
||||
|
||||
/// The routes for Jobs, Jobs, Jobs
|
||||
let webApp =
|
||||
choose
|
||||
[ GET >=> choose
|
||||
[ pathScan "/api/profile/%s" Profile.get
|
||||
path "/api/profile" >=> Profile.get ""
|
||||
path "/" >=> Vue.app
|
||||
Files.browse "wwwroot/"
|
||||
]
|
||||
// PUT >=> choose
|
||||
// [ ]
|
||||
// PATCH >=> choose
|
||||
// [ ]
|
||||
POST >=> choose
|
||||
[ path "/api/citizen/log-on" >=> Citizen.logOn
|
||||
]
|
||||
Error.notFound
|
||||
]
|
|
@ -1,31 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Domain.fs" />
|
||||
<Compile Include="Extensions.fs" />
|
||||
<Compile Include="Data.fs" />
|
||||
<Compile Include="ViewModels.fs" />
|
||||
<Compile Include="Auth.fs" />
|
||||
<Compile Include="Handlers.fs" />
|
||||
<Compile Include="Program.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FSharp.Json" Version="0.4.0" />
|
||||
<PackageReference Include="JWT" Version="7.2.1" />
|
||||
<PackageReference Include="Markdig" Version="0.21.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.8" />
|
||||
<PackageReference Include="Nanoid" Version="2.1.0" />
|
||||
<PackageReference Include="Npgsql.FSharp" Version="3.7.0" />
|
||||
<PackageReference Include="Suave" Version="2.5.6" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,18 +0,0 @@
|
|||
module JobsJobsJobs.Api.App
|
||||
|
||||
// Learn more about F# at http://fsharp.org
|
||||
|
||||
open System
|
||||
open Suave
|
||||
|
||||
[<EntryPoint>]
|
||||
let main argv =
|
||||
{ defaultConfig with
|
||||
bindings = [ HttpBinding.createSimple HTTP "127.0.0.1" 3002; HttpBinding.createSimple HTTP "::1" 3002 ]
|
||||
// errorHandler = Handlers.Error.error
|
||||
// serverKey = config.serverKey
|
||||
// cookieSerialiser = FSharpJsonCookieSerialiser()
|
||||
// homeFolder = Some "./wwwroot/"
|
||||
}
|
||||
|> (flip startWebServer) Handlers.webApp
|
||||
0
|
|
@ -1,27 +0,0 @@
|
|||
module JobsJobsJobs.Api.ViewModels
|
||||
|
||||
// fsharplint:disable RecordFieldNames MemberNames
|
||||
|
||||
/// View models uses for /api/citizen routes
|
||||
module Citizen =
|
||||
|
||||
open FSharp.Json
|
||||
|
||||
/// The payload for the log on route
|
||||
type LogOn = {
|
||||
/// The access token obtained from No Agenda Social
|
||||
accessToken : string
|
||||
}
|
||||
|
||||
/// The variables we need from the account information we get from No Agenda Social
|
||||
type MastodonAccount = {
|
||||
/// The user name (what we store as naUser)
|
||||
username : string
|
||||
/// The account name; will be the same as username for local (non-federated) accounts
|
||||
acct : string
|
||||
/// The user's display name as it currently shows on No Agenda Social
|
||||
[<JsonField "display_name">]
|
||||
displayName : string
|
||||
/// The user's profile URL
|
||||
url : string
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
|
@ -1,18 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/typescript'
|
||||
],
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||
}
|
||||
}
|
26
src/jobs-jobs-jobs/.gitignore
vendored
26
src/jobs-jobs-jobs/.gitignore
vendored
|
@ -1,26 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
dev-client.txt
|
||||
src/auth/config.ts
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
29663
src/jobs-jobs-jobs/package-lock.json
generated
29663
src/jobs-jobs-jobs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"name": "jobs-jobs-jobs",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --port 3005",
|
||||
"build": "vue-cli-service build --modern",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"vue": "^3.0.0-0",
|
||||
"vue-class-component": "^8.0.0-0",
|
||||
"vue-router": "^4.0.0-0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^4.8.0",
|
||||
"@typescript-eslint/parser": "^4.8.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "^4.5.4",
|
||||
"@vue/cli-plugin-typescript": "^4.5.4",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0-0",
|
||||
"@vue/eslint-config-typescript": "^7.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.0.0",
|
||||
"eslint-plugin-vue": "^7.0.0-0",
|
||||
"typescript": "~4.0.0"
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
Binary file not shown.
|
@ -1,44 +0,0 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<div id="nav">
|
||||
<router-link to="/">Home</router-link> |
|
||||
<router-link to="/about">About</router-link> |
|
||||
<a href="#" @click.stop="authorize">Log On</a>
|
||||
</div>
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { authorize } from '@/auth'
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
authorize
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
#nav {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
#nav a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
#nav a.router-link-exact-active {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
|
@ -1,100 +0,0 @@
|
|||
import { ref } from 'vue'
|
||||
import { Profile } from './types'
|
||||
|
||||
/**
|
||||
* Jobs, Jobs, Jobs API interface
|
||||
*
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @version 1
|
||||
*/
|
||||
|
||||
/** The base URL for the Jobs, Jobs, Jobs API */
|
||||
const API_URL = `${location.protocol}//${location.host}/api`
|
||||
|
||||
/** Local storage key for the Jobs, Jobs, Jobs access token */
|
||||
const JJJ_TOKEN = 'jjj-token'
|
||||
|
||||
/** HTTP status for "No Content"; used by the API to indicate a valid query with no results vs. 404 (invalid URL) */
|
||||
const NO_CONTENT = 204
|
||||
|
||||
/**
|
||||
* A holder for the JSON Web Token (JWT) returned from Jobs, Jobs, Jobs
|
||||
*/
|
||||
class JwtHolder {
|
||||
private jwt: string | null = null
|
||||
|
||||
/**
|
||||
* Get the current token (refreshing from local storage if needed).
|
||||
*/
|
||||
get token(): string | null {
|
||||
if (!this.jwt) this.jwt = localStorage.getItem(JJJ_TOKEN)
|
||||
return this.jwt
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current token (both here and in local storage).
|
||||
*
|
||||
* @param tokn The token to be set
|
||||
*/
|
||||
set token(tokn: string | null) {
|
||||
if (tokn) localStorage.setItem(JJJ_TOKEN, tokn); else localStorage.removeItem(JJJ_TOKEN)
|
||||
this.jwt = tokn
|
||||
}
|
||||
|
||||
get hasToken(): boolean {
|
||||
return this.token !== null
|
||||
}
|
||||
}
|
||||
|
||||
/** The user's current JWT */
|
||||
const jwt = new JwtHolder()
|
||||
|
||||
/**
|
||||
* Execute an HTTP request using the fetch API.
|
||||
*
|
||||
* @param url The URL to which the request should be made
|
||||
* @param method The HTTP method for the request (defaults to GET)
|
||||
* @param payload The payload to send along with the request (defaults to none)
|
||||
* @returns The response (if the request is successful)
|
||||
* @throws An error (if the request is unsuccessful)
|
||||
*/
|
||||
export async function doRequest(url: string, method?: string, payload?: string) {
|
||||
const headers: [string, string][] = [ [ 'Content-Type', 'application/json' ] ]
|
||||
if (jwt.hasToken) headers.push([ 'Authorization', `Bearer ${jwt.token}`])
|
||||
const options: RequestInit = {
|
||||
method: method || 'GET',
|
||||
headers: headers
|
||||
}
|
||||
if (method === 'POST' && payload) options.body = payload
|
||||
const actualUrl = (options.method === 'GET' && payload) ? `url?${payload}` : url
|
||||
const resp = await fetch(actualUrl, options)
|
||||
if (resp.ok) return resp
|
||||
throw new Error(`Error executing API request: ${resp.status} ~ ${resp.statusText}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize with Jobs, Jobs, Jobs using a No Agenda Social token.
|
||||
*
|
||||
* @param nasToken The token obtained from No Agenda Social
|
||||
* @returns True if it is successful
|
||||
*/
|
||||
export async function jjjAuthorize(nasToken: string): Promise<boolean> {
|
||||
const resp = await doRequest(`${API_URL}/citizen/log-on`, 'POST', JSON.stringify({ accessToken: nasToken }))
|
||||
const jjjToken = await resp.json()
|
||||
jwt.token = jjjToken.accessToken
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the employment profile for the current user.
|
||||
*
|
||||
* @returns The profile if it is found; undefined otherwise
|
||||
*/
|
||||
export async function userProfile(): Promise<Profile | undefined> {
|
||||
const resp = await doRequest(`${API_URL}/profile`)
|
||||
if (resp.status === NO_CONTENT) {
|
||||
return undefined
|
||||
}
|
||||
const profile = await resp.json()
|
||||
return profile as Profile
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
/**
|
||||
* Client-side Type Definitions for Jobs, Jobs, Jobs.
|
||||
*
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @version 1
|
||||
*/
|
||||
|
||||
/**
|
||||
* A continent (one of the 7).
|
||||
*/
|
||||
export interface Continent {
|
||||
/** The ID of the continent */
|
||||
id: string
|
||||
|
||||
/** The name of the continent */
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A user's employment profile.
|
||||
*/
|
||||
export interface Profile {
|
||||
/** The ID of the user to whom the profile applies */
|
||||
citizenId: string
|
||||
|
||||
/** Whether this user is actively seeking employment */
|
||||
seekingEmployment: boolean
|
||||
|
||||
/** Whether information from this profile should appear in the public anonymous list of available skills */
|
||||
isPublic: boolean
|
||||
|
||||
/** The continent on which the user is seeking employment */
|
||||
continent: Continent
|
||||
|
||||
/** The region within that continent where the user would prefer to work */
|
||||
region: string
|
||||
|
||||
/** Whether the user is looking for remote work */
|
||||
remoteWork: boolean
|
||||
|
||||
/** Whether the user is looking for full-time work */
|
||||
fullTime: boolean
|
||||
|
||||
/** The user's professional biography */
|
||||
biography: string
|
||||
|
||||
/** When this profile was last updated */
|
||||
lastUpdatedOn: number
|
||||
|
||||
/** The user's experience */
|
||||
experience?: string
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
|
@ -1,61 +0,0 @@
|
|||
/**
|
||||
* Authentication and Authorization.
|
||||
*
|
||||
* This contains authentication and authorization functions needed to permit secure access to the application.
|
||||
*
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @version 1
|
||||
*/
|
||||
import { CLIENT_SECRET } from './config'
|
||||
import { doRequest, jjjAuthorize } from '../api'
|
||||
|
||||
/** Client ID for Jobs, Jobs, Jobs */
|
||||
const CLIENT_ID = '6Ook3LBff00dOhyBgbf4eXSqIpAroK72aioIdGaDqxs'
|
||||
|
||||
/** No Agenda Social's base URL */
|
||||
const NAS_URL = 'https://noagendasocial.com/'
|
||||
|
||||
/** The base URL for Jobs, Jobs, Jobs */
|
||||
const JJJ_URL = `${location.protocol}//${location.host}/`
|
||||
|
||||
/**
|
||||
* Authorize access to this application from No Agenda Social.
|
||||
*
|
||||
* This is the first step in a 2-step log on process; this step will prompt the user to authorize Jobs, Jobs, Jobs to
|
||||
* get information from their No Agenda Social profile. Once that authorization has been granted, we receive an access
|
||||
* code which we can use to request a full token.
|
||||
*/
|
||||
export function authorize() {
|
||||
const params = new URLSearchParams([
|
||||
[ 'client_id', CLIENT_ID ],
|
||||
[ 'scope', 'read' ],
|
||||
[ 'redirect_uri', `${JJJ_URL}user/authorized` ],
|
||||
[ 'response_type', 'code' ]
|
||||
]).toString()
|
||||
location.assign(`${NAS_URL}oauth/authorize?${params}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log on a user with an authorzation code.
|
||||
*
|
||||
* @param authCode The authorization code obtained from No Agenda Social
|
||||
*/
|
||||
export async function logOn(authCode: string): Promise<string> {
|
||||
try {
|
||||
const resp = await doRequest(`${NAS_URL}oauth/token`, 'POST',
|
||||
JSON.stringify({
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
redirect_uri: `${JJJ_URL}user/authorized`,
|
||||
grant_type: 'authorization_code',
|
||||
code: authCode,
|
||||
scope: 'read'
|
||||
})
|
||||
)
|
||||
const token = await resp.json()
|
||||
await jjjAuthorize(token.access_token)
|
||||
return ''
|
||||
} catch (e) {
|
||||
return `${e}`
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
<template>
|
||||
<div class="hello">
|
||||
<h1>
|
||||
Jobs, Jobs, Jobs<br>
|
||||
<small><small><small><em>
|
||||
(and Jobs - <a class="audio" @click="playJobs">Let's Vote for Jobs!</a>)
|
||||
</em></small></small></small>
|
||||
</h1>
|
||||
<p>
|
||||
Future home of No Agenda Jobs, where citizens of Gitmo Nation can assist one another in finding or enhancing
|
||||
their employment. This will enable them to continue providing value for value to Adam and John, as they continue
|
||||
their work deconstructing the misinformation that passes for news on a day-to-day basis.
|
||||
</p>
|
||||
<p>
|
||||
Do you not understand the terms in the paragraph above? No worries; just head over to
|
||||
<a href="https://noagendashow.net">The Best Podcast in the Universe</a> <em><a class="audio" @click="playTrue">(it's true!)</a></em> and find out what
|
||||
you’re missing.
|
||||
</p>
|
||||
<audio ref="jobsAudio">
|
||||
<source src="/pelosi-jobs.mp3">
|
||||
</audio>
|
||||
<audio ref="trueAudio">
|
||||
<source src="/thats-true.mp3">
|
||||
</audio>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
msg: String
|
||||
},
|
||||
setup() {
|
||||
const jobsAudio = ref(null)
|
||||
const trueAudio = ref(null)
|
||||
|
||||
const playJobs = () => jobsAudio.value.play()
|
||||
const playTrue = () => trueAudio.value.play()
|
||||
return {
|
||||
jobsAudio,
|
||||
trueAudio,
|
||||
playJobs,
|
||||
playTrue
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
a.audio {
|
||||
color: inherit;
|
||||
border-bottom: dotted 1px lightgray;
|
||||
}
|
||||
a.audio:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +0,0 @@
|
|||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
|
@ -1,39 +0,0 @@
|
|||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import Home from '../views/Home.vue'
|
||||
import Welcome from '../views/citizen/Welcome.vue'
|
||||
import Authorized from '../views/user/Authorized.vue'
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
||||
},
|
||||
{
|
||||
path: '/user/authorized',
|
||||
name: 'Authorized',
|
||||
component: Authorized,
|
||||
props: (route) => ({ code: route.query.code })
|
||||
},
|
||||
{
|
||||
path: '/citizen/welcome',
|
||||
name: 'Welcome',
|
||||
component: Welcome
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
5
src/jobs-jobs-jobs/src/shims-vue.d.ts
vendored
5
src/jobs-jobs-jobs/src/shims-vue.d.ts
vendored
|
@ -1,5 +0,0 @@
|
|||
declare module '*.vue' {
|
||||
import { defineComponent } from 'vue'
|
||||
const component: ReturnType<typeof defineComponent>
|
||||
export default component
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
|
@ -1,75 +0,0 @@
|
|||
<template>
|
||||
<div class="hello">
|
||||
<h1>
|
||||
Jobs, Jobs, Jobs<br>
|
||||
<small><small><small><em>
|
||||
(and Jobs - <a class="audio" @click="playJobs">Let's Vote for Jobs!</a>)
|
||||
</em></small></small></small>
|
||||
</h1>
|
||||
<p>
|
||||
Future home of No Agenda Jobs, where citizens of Gitmo Nation can assist one another in finding or enhancing
|
||||
their employment. This will enable them to continue providing value for value to Adam and John, as they continue
|
||||
their work deconstructing the misinformation that passes for news on a day-to-day basis.
|
||||
</p>
|
||||
<p>
|
||||
Do you not understand the terms in the paragraph above? No worries; just head over to
|
||||
<a href="https://noagendashow.net">The Best Podcast in the Universe</a> <em><a class="audio" @click="playTrue">(it's true!)</a></em> and find out what
|
||||
you’re missing.
|
||||
</p>
|
||||
<audio ref="jobsAudio">
|
||||
<source src="/pelosi-jobs.mp3">
|
||||
</audio>
|
||||
<audio ref="trueAudio">
|
||||
<source src="/thats-true.mp3">
|
||||
</audio>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
msg: String
|
||||
},
|
||||
setup() {
|
||||
const jobsAudio = ref(null)
|
||||
const trueAudio = ref(null)
|
||||
|
||||
const playJobs = () => jobsAudio.value.play()
|
||||
const playTrue = () => trueAudio.value.play()
|
||||
return {
|
||||
jobsAudio,
|
||||
trueAudio,
|
||||
playJobs,
|
||||
playTrue
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
a.audio {
|
||||
color: inherit;
|
||||
border-bottom: dotted 1px lightgray;
|
||||
}
|
||||
a.audio:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -1,27 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<p>Welcome!</p>
|
||||
<p>Profile Established: <strong><span v-if="profile?.value">Yes</span><span v-else>No</span></strong></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { onBeforeMount, ref } from 'vue'
|
||||
|
||||
import { userProfile } from '@/api'
|
||||
import { Profile } from '@/api/types'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const profile = ref<Profile | undefined>(undefined)
|
||||
|
||||
onBeforeMount(async () => {
|
||||
profile.value = await userProfile()
|
||||
})
|
||||
|
||||
return {
|
||||
profile
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,30 +0,0 @@
|
|||
<template>
|
||||
<p>{{message}}</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { logOn } from '@/auth'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
code: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const message = ref('Logging you on with No Agenda Social...')
|
||||
return {
|
||||
message
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
const result = await logOn(this.code)
|
||||
if (result === '') {
|
||||
this.$router.push('/citizen/welcome')
|
||||
} else {
|
||||
this.message = `Unable to log on via No Agenda Social:\n${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,41 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": false,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user