Log On works
- E-mail address must be unique - Remove references to Mastodon and instances - Move profile edit from /citizen to /profile - Change indexes to BTREE - Eliminate pug
This commit is contained in:
parent
12fdf368e0
commit
2d5e391b60
631
src/JobsJobsJobs/App/package-lock.json
generated
631
src/JobsJobsJobs/App/package-lock.json
generated
|
@ -46,8 +46,7 @@
|
||||||
"eslint-plugin-vue": "^9.2.0",
|
"eslint-plugin-vue": "^9.2.0",
|
||||||
"sass": "~1.37.0",
|
"sass": "~1.37.0",
|
||||||
"sass-loader": "^10.0.0",
|
"sass-loader": "^10.0.0",
|
||||||
"typescript": "~4.5.0",
|
"typescript": "~4.5.0"
|
||||||
"vue-cli-plugin-pug": "~2.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@achrinza/node-ipc": {
|
"node_modules/@achrinza/node-ipc": {
|
||||||
|
@ -3898,18 +3897,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/asap": {
|
|
||||||
"version": "2.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
|
||||||
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/assert-never": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "2.6.4",
|
"version": "2.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||||
|
@ -4028,18 +4015,6 @@
|
||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-walk": {
|
|
||||||
"version": "3.0.0-canary-5",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
|
|
||||||
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/types": "^7.9.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
@ -4399,15 +4374,6 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/character-parser": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
|
|
||||||
"integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"is-regex": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||||
|
@ -4739,16 +4705,6 @@
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/constantinople": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.6.0",
|
|
||||||
"@babel/types": "^7.6.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
@ -5521,12 +5477,6 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/doctypes": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
|
|
||||||
"integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/dom-converter": {
|
"node_modules/dom-converter": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
||||||
|
@ -8072,28 +8022,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-expression": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"acorn": "^7.1.1",
|
|
||||||
"object-assign": "^4.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-expression/node_modules/acorn": {
|
|
||||||
"version": "7.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
|
||||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"acorn": "bin/acorn"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
@ -8202,12 +8130,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-promise": {
|
|
||||||
"version": "2.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
|
||||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/is-regex": {
|
"node_modules/is-regex": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
||||||
|
@ -8398,12 +8320,6 @@
|
||||||
"node": ">=0.6.0"
|
"node": ">=0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-stringify": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
|
|
||||||
"integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
@ -8482,16 +8398,6 @@
|
||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jstransformer": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"is-promise": "^2.0.0",
|
|
||||||
"promise": "^7.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/kind-of": {
|
"node_modules/kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
@ -10487,15 +10393,6 @@
|
||||||
"webpack": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0"
|
"webpack": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/promise": {
|
|
||||||
"version": "7.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
|
||||||
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"asap": "~2.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
@ -10524,142 +10421,6 @@
|
||||||
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
|
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/pug": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"pug-code-gen": "^3.0.2",
|
|
||||||
"pug-filters": "^4.0.0",
|
|
||||||
"pug-lexer": "^5.0.1",
|
|
||||||
"pug-linker": "^4.0.0",
|
|
||||||
"pug-load": "^3.0.0",
|
|
||||||
"pug-parser": "^6.0.0",
|
|
||||||
"pug-runtime": "^3.0.1",
|
|
||||||
"pug-strip-comments": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-attrs": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"constantinople": "^4.0.1",
|
|
||||||
"js-stringify": "^1.0.2",
|
|
||||||
"pug-runtime": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-code-gen": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"constantinople": "^4.0.1",
|
|
||||||
"doctypes": "^1.1.0",
|
|
||||||
"js-stringify": "^1.0.2",
|
|
||||||
"pug-attrs": "^3.0.0",
|
|
||||||
"pug-error": "^2.0.0",
|
|
||||||
"pug-runtime": "^3.0.0",
|
|
||||||
"void-elements": "^3.1.0",
|
|
||||||
"with": "^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-error": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/pug-filters": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"constantinople": "^4.0.1",
|
|
||||||
"jstransformer": "1.0.0",
|
|
||||||
"pug-error": "^2.0.0",
|
|
||||||
"pug-walk": "^2.0.0",
|
|
||||||
"resolve": "^1.15.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-lexer": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"character-parser": "^2.2.0",
|
|
||||||
"is-expression": "^4.0.0",
|
|
||||||
"pug-error": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-linker": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"pug-error": "^2.0.0",
|
|
||||||
"pug-walk": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-load": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"object-assign": "^4.1.1",
|
|
||||||
"pug-walk": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-parser": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"pug-error": "^2.0.0",
|
|
||||||
"token-stream": "1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-plain-loader": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-plain-loader/-/pug-plain-loader-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-1nYgIJLaahRuHJHhzSPODV44aZfb00bO7kiJiMkke6Hj4SVZftuvx6shZ4BOokk50dJc2RSFqNUBOlus0dniFQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"loader-utils": "^1.1.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"pug": "^2.0.0 || ^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-runtime": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/pug-strip-comments": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"pug-error": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-walk": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
@ -10756,58 +10517,6 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/raw-loader": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"loader-utils": "^2.0.0",
|
|
||||||
"schema-utils": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.13.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/webpack"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"webpack": "^4.0.0 || ^5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/raw-loader/node_modules/loader-utils": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"big.js": "^5.2.2",
|
|
||||||
"emojis-list": "^3.0.0",
|
|
||||||
"json5": "^2.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/raw-loader/node_modules/schema-utils": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.8",
|
|
||||||
"ajv": "^6.12.5",
|
|
||||||
"ajv-keywords": "^3.5.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.13.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/webpack"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/read-pkg": {
|
"node_modules/read-pkg": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
||||||
|
@ -12050,12 +11759,6 @@
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/token-stream": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/totalist": {
|
"node_modules/totalist": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
|
||||||
|
@ -12452,15 +12155,6 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/void-elements": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
|
||||||
"integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.2.37",
|
"version": "3.2.37",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.37.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.37.tgz",
|
||||||
|
@ -12473,17 +12167,6 @@
|
||||||
"@vue/shared": "3.2.37"
|
"@vue/shared": "3.2.37"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-cli-plugin-pug": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-cli-plugin-pug/-/vue-cli-plugin-pug-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-Nnl5pkHqNDMkPTvOX4iidFSUs0sn2A5JGsQBVZ70Wm2aXqbK/B3jvEFOWok5/y9U/aWeZLU05PwYVYc7nTUK2Q==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"pug": "^3.0.0",
|
|
||||||
"pug-plain-loader": "^1.0.0",
|
|
||||||
"raw-loader": "^4.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-demi": {
|
"node_modules/vue-demi": {
|
||||||
"version": "0.13.2",
|
"version": "0.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.2.tgz",
|
||||||
|
@ -13281,21 +12964,6 @@
|
||||||
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
|
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/with": {
|
|
||||||
"version": "7.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
|
|
||||||
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.9.6",
|
|
||||||
"@babel/types": "^7.9.6",
|
|
||||||
"assert-never": "^1.2.1",
|
|
||||||
"babel-walk": "3.0.0-canary-5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||||
|
@ -16353,18 +16021,6 @@
|
||||||
"es-shim-unscopables": "^1.0.0"
|
"es-shim-unscopables": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"asap": {
|
|
||||||
"version": "2.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
|
||||||
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"assert-never": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"async": {
|
"async": {
|
||||||
"version": "2.6.4",
|
"version": "2.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||||
|
@ -16445,15 +16101,6 @@
|
||||||
"@babel/helper-define-polyfill-provider": "^0.3.1"
|
"@babel/helper-define-polyfill-provider": "^0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-walk": {
|
|
||||||
"version": "3.0.0-canary-5",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
|
|
||||||
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@babel/types": "^7.9.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
@ -16715,15 +16362,6 @@
|
||||||
"supports-color": "^5.3.0"
|
"supports-color": "^5.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"character-parser": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
|
|
||||||
"integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"is-regex": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chokidar": {
|
"chokidar": {
|
||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||||
|
@ -16981,16 +16619,6 @@
|
||||||
"bluebird": "^3.1.1"
|
"bluebird": "^3.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"constantinople": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@babel/parser": "^7.6.0",
|
|
||||||
"@babel/types": "^7.6.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"content-disposition": {
|
"content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
@ -17522,12 +17150,6 @@
|
||||||
"esutils": "^2.0.2"
|
"esutils": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"doctypes": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
|
|
||||||
"integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"dom-converter": {
|
"dom-converter": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
||||||
|
@ -19381,24 +19003,6 @@
|
||||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"is-expression": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"acorn": "^7.1.1",
|
|
||||||
"object-assign": "^4.1.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"acorn": {
|
|
||||||
"version": "7.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
|
||||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"is-extglob": {
|
"is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
@ -19471,12 +19075,6 @@
|
||||||
"isobject": "^3.0.1"
|
"isobject": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"is-promise": {
|
|
||||||
"version": "2.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
|
||||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"is-regex": {
|
"is-regex": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
||||||
|
@ -19615,12 +19213,6 @@
|
||||||
"integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==",
|
"integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"js-stringify": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
|
|
||||||
"integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"js-tokens": {
|
"js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
@ -19682,16 +19274,6 @@
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jstransformer": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"is-promise": "^2.0.0",
|
|
||||||
"promise": "^7.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
@ -21141,15 +20723,6 @@
|
||||||
"log-update": "^2.3.0"
|
"log-update": "^2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"promise": {
|
|
||||||
"version": "7.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
|
||||||
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"asap": "~2.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"proxy-addr": {
|
"proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
@ -21174,139 +20747,6 @@
|
||||||
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
|
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"pug": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"pug-code-gen": "^3.0.2",
|
|
||||||
"pug-filters": "^4.0.0",
|
|
||||||
"pug-lexer": "^5.0.1",
|
|
||||||
"pug-linker": "^4.0.0",
|
|
||||||
"pug-load": "^3.0.0",
|
|
||||||
"pug-parser": "^6.0.0",
|
|
||||||
"pug-runtime": "^3.0.1",
|
|
||||||
"pug-strip-comments": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pug-attrs": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"constantinople": "^4.0.1",
|
|
||||||
"js-stringify": "^1.0.2",
|
|
||||||
"pug-runtime": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pug-code-gen": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"constantinople": "^4.0.1",
|
|
||||||
"doctypes": "^1.1.0",
|
|
||||||
"js-stringify": "^1.0.2",
|
|
||||||
"pug-attrs": "^3.0.0",
|
|
||||||
"pug-error": "^2.0.0",
|
|
||||||
"pug-runtime": "^3.0.0",
|
|
||||||
"void-elements": "^3.1.0",
|
|
||||||
"with": "^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pug-error": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"pug-filters": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"constantinople": "^4.0.1",
|
|
||||||
"jstransformer": "1.0.0",
|
|
||||||
"pug-error": "^2.0.0",
|
|
||||||
"pug-walk": "^2.0.0",
|
|
||||||
"resolve": "^1.15.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pug-lexer": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"character-parser": "^2.2.0",
|
|
||||||
"is-expression": "^4.0.0",
|
|
||||||
"pug-error": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pug-linker": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"pug-error": "^2.0.0",
|
|
||||||
"pug-walk": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pug-load": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"object-assign": "^4.1.1",
|
|
||||||
"pug-walk": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pug-parser": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"pug-error": "^2.0.0",
|
|
||||||
"token-stream": "1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pug-plain-loader": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-plain-loader/-/pug-plain-loader-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-1nYgIJLaahRuHJHhzSPODV44aZfb00bO7kiJiMkke6Hj4SVZftuvx6shZ4BOokk50dJc2RSFqNUBOlus0dniFQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"loader-utils": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pug-runtime": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"pug-strip-comments": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"pug-error": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pug-walk": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"pump": {
|
"pump": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
@ -21373,40 +20813,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"raw-loader": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"loader-utils": "^2.0.0",
|
|
||||||
"schema-utils": "^3.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"loader-utils": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"big.js": "^5.2.2",
|
|
||||||
"emojis-list": "^3.0.0",
|
|
||||||
"json5": "^2.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schema-utils": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@types/json-schema": "^7.0.8",
|
|
||||||
"ajv": "^6.12.5",
|
|
||||||
"ajv-keywords": "^3.5.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"read-pkg": {
|
"read-pkg": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
||||||
|
@ -22336,12 +21742,6 @@
|
||||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"token-stream": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"totalist": {
|
"totalist": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
|
||||||
|
@ -22628,12 +22028,6 @@
|
||||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"void-elements": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
|
||||||
"integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"vue": {
|
"vue": {
|
||||||
"version": "3.2.37",
|
"version": "3.2.37",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.37.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.37.tgz",
|
||||||
|
@ -22646,17 +22040,6 @@
|
||||||
"@vue/shared": "3.2.37"
|
"@vue/shared": "3.2.37"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue-cli-plugin-pug": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-cli-plugin-pug/-/vue-cli-plugin-pug-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-Nnl5pkHqNDMkPTvOX4iidFSUs0sn2A5JGsQBVZ70Wm2aXqbK/B3jvEFOWok5/y9U/aWeZLU05PwYVYc7nTUK2Q==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"pug": "^3.0.0",
|
|
||||||
"pug-plain-loader": "^1.0.0",
|
|
||||||
"raw-loader": "^4.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vue-demi": {
|
"vue-demi": {
|
||||||
"version": "0.13.2",
|
"version": "0.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.2.tgz",
|
||||||
|
@ -23237,18 +22620,6 @@
|
||||||
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
|
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"with": {
|
|
||||||
"version": "7.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
|
|
||||||
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@babel/parser": "^7.9.6",
|
|
||||||
"@babel/types": "^7.9.6",
|
|
||||||
"assert-never": "^1.2.1",
|
|
||||||
"babel-walk": "3.0.0-canary-5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"word-wrap": {
|
"word-wrap": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||||
|
|
|
@ -46,7 +46,6 @@
|
||||||
"eslint-plugin-vue": "^9.2.0",
|
"eslint-plugin-vue": "^9.2.0",
|
||||||
"sass": "~1.37.0",
|
"sass": "~1.37.0",
|
||||||
"sass-loader": "^10.0.0",
|
"sass-loader": "^10.0.0",
|
||||||
"typescript": "~4.5.0",
|
"typescript": "~4.5.0"
|
||||||
"vue-cli-plugin-pug": "~2.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,13 +52,13 @@ export function yesOrNo (cond : boolean) : string {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the display name for a citizen (the first available among real, display, or Mastodon handle)
|
* Get the display name for a citizen
|
||||||
*
|
*
|
||||||
* @param cit The citizen
|
* @param cit The citizen
|
||||||
* @returns The citizen's display name
|
* @returns The citizen's display name
|
||||||
*/
|
*/
|
||||||
export function citizenName (cit : Citizen) : string {
|
export function citizenName (cit : Citizen) : string {
|
||||||
return cit.realName ?? cit.displayName ?? cit.mastodonUser
|
return cit.displayName ?? `${cit.firstName} ${cit.lastName}`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,12 @@ import {
|
||||||
CitizenRegistrationForm,
|
CitizenRegistrationForm,
|
||||||
Continent,
|
Continent,
|
||||||
Count,
|
Count,
|
||||||
Instance,
|
|
||||||
Listing,
|
Listing,
|
||||||
ListingExpireForm,
|
ListingExpireForm,
|
||||||
ListingForm,
|
ListingForm,
|
||||||
ListingForView,
|
ListingForView,
|
||||||
ListingSearch,
|
ListingSearch,
|
||||||
|
LogOnForm,
|
||||||
LogOnSuccess,
|
LogOnSuccess,
|
||||||
Profile,
|
Profile,
|
||||||
ProfileForm,
|
ProfileForm,
|
||||||
|
@ -108,8 +108,12 @@ export default {
|
||||||
* @param form The registration details for the citizen
|
* @param form The registration details for the citizen
|
||||||
* @returns True if the registration was successful, an error message if it was not
|
* @returns True if the registration was successful, an error message if it was not
|
||||||
*/
|
*/
|
||||||
register: async (form : CitizenRegistrationForm) : Promise<boolean | string> =>
|
register: async (form : CitizenRegistrationForm) : Promise<boolean | string> => {
|
||||||
apiSend(await fetch(apiUrl("citizen/register"), reqInit("POST", undefined, form)), "registering citizen"),
|
const resp = await fetch(apiUrl("citizen/register"), reqInit("POST", undefined, form))
|
||||||
|
if (resp.status === 200) return true
|
||||||
|
if (resp.status === 409) return "There is already an account registered to the e-mail address provided"
|
||||||
|
return `Error registering citizen - ${await resp.text()}`
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm an account by verifying a token they received via e-mail
|
* Confirm an account by verifying a token they received via e-mail
|
||||||
|
@ -142,12 +146,11 @@ export default {
|
||||||
/**
|
/**
|
||||||
* Log a citizen on
|
* Log a citizen on
|
||||||
*
|
*
|
||||||
* @param abbr The abbreviation of the Mastodon instance that issued the code
|
* @param form The e-mail address and password provided by the user
|
||||||
* @param code The authorization code from Mastodon
|
|
||||||
* @returns The user result, or an error
|
* @returns The user result, or an error
|
||||||
*/
|
*/
|
||||||
logOn: async (abbr : string, code : string) : Promise<LogOnSuccess | string> => {
|
logOn: async (form : LogOnForm) : Promise<LogOnSuccess | string> => {
|
||||||
const resp = await fetch(apiUrl(`citizen/log-on/${abbr}/${code}`), { method: "GET", mode: "cors" })
|
const resp = await fetch(apiUrl("citizen/log-on"), reqInit("POST", undefined, form))
|
||||||
if (resp.status === 200) return await resp.json() as LogOnSuccess
|
if (resp.status === 200) return await resp.json() as LogOnSuccess
|
||||||
return `Error logging on - ${await resp.text()}`
|
return `Error logging on - ${await resp.text()}`
|
||||||
},
|
},
|
||||||
|
@ -184,18 +187,6 @@ export default {
|
||||||
apiResult<Continent[]>(await fetch(apiUrl("continents"), { method: "GET" }), "retrieving continents")
|
apiResult<Continent[]>(await fetch(apiUrl("continents"), { method: "GET" }), "retrieving continents")
|
||||||
},
|
},
|
||||||
|
|
||||||
/** API functions for instances */
|
|
||||||
instances: {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all Mastodon instances we support
|
|
||||||
*
|
|
||||||
* @returns All instances, or an error
|
|
||||||
*/
|
|
||||||
all: async () : Promise<Instance[] | string | undefined> =>
|
|
||||||
apiResult<Instance[]>(await fetch(apiUrl("instances"), { method: "GET" }), "retrieving Mastodon instances")
|
|
||||||
},
|
|
||||||
|
|
||||||
/** API functions for job listings */
|
/** API functions for job listings */
|
||||||
listings: {
|
listings: {
|
||||||
|
|
||||||
|
|
|
@ -3,20 +3,20 @@
|
||||||
export interface Citizen {
|
export interface Citizen {
|
||||||
/** The ID of the user */
|
/** The ID of the user */
|
||||||
id : string
|
id : string
|
||||||
/** The abbreviation of the instance where this citizen is based */
|
|
||||||
instance : string
|
|
||||||
/** The handle by which the user is known on Mastodon */
|
|
||||||
mastodonUser : string
|
|
||||||
/** The user's display name from Mastodon (updated every login) */
|
|
||||||
displayName : string | undefined
|
|
||||||
/** The user's real name */
|
|
||||||
realName : string | undefined
|
|
||||||
/** The URL for the user's Mastodon profile */
|
|
||||||
profileUrl : string
|
|
||||||
/** When the user joined Jobs, Jobs, Jobs (date) */
|
/** When the user joined Jobs, Jobs, Jobs (date) */
|
||||||
joinedOn : string
|
joinedOn : string
|
||||||
/** When the user last logged in (date) */
|
/** When the user last logged in (date) */
|
||||||
lastSeenOn : string
|
lastSeenOn : string
|
||||||
|
/** The citizen's e-mail address */
|
||||||
|
email : string
|
||||||
|
/** The citizen's first name */
|
||||||
|
firstName : string
|
||||||
|
/** The citizen's last name */
|
||||||
|
lastName : string
|
||||||
|
/** The citizen's display name */
|
||||||
|
displayName : string | undefined
|
||||||
|
/** The user's real name */
|
||||||
|
otherContacts : any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The data required to register as a user */
|
/** The data required to register as a user */
|
||||||
|
@ -49,22 +49,6 @@ export interface Count {
|
||||||
count : number
|
count : number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The Mastodon instance data provided via the Jobs, Jobs, Jobs API */
|
|
||||||
export interface Instance {
|
|
||||||
/** The name of the instance */
|
|
||||||
name : string
|
|
||||||
/** The URL for this instance */
|
|
||||||
url : string
|
|
||||||
/** The abbreviation used in the URL to distinguish this instance's return codes */
|
|
||||||
abbr : string
|
|
||||||
/** The client ID (assigned by the Mastodon server) */
|
|
||||||
clientId : string
|
|
||||||
/** Whether this instance is enabled */
|
|
||||||
isEnabled : boolean
|
|
||||||
/** If disabled, the reason why it is disabled */
|
|
||||||
reason : string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A job listing */
|
/** A job listing */
|
||||||
export interface Listing {
|
export interface Listing {
|
||||||
/** The ID of the job listing */
|
/** The ID of the job listing */
|
||||||
|
@ -139,6 +123,14 @@ export interface ListingSearch {
|
||||||
text : string | undefined
|
text : string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Data used to log on */
|
||||||
|
export class LogOnForm {
|
||||||
|
/** The e-mail address of a citizen's account */
|
||||||
|
email = ""
|
||||||
|
/** The password for that account */
|
||||||
|
password = ""
|
||||||
|
}
|
||||||
|
|
||||||
/** A successful logon */
|
/** A successful logon */
|
||||||
export interface LogOnSuccess {
|
export interface LogOnSuccess {
|
||||||
/** The JSON Web Token (JWT) to use for API access */
|
/** The JSON Web Token (JWT) to use for API access */
|
||||||
|
@ -191,8 +183,6 @@ export class ProfileForm {
|
||||||
isSeekingEmployment = false
|
isSeekingEmployment = false
|
||||||
/** Whether this profile should appear in the public search */
|
/** Whether this profile should appear in the public search */
|
||||||
isPublic = false
|
isPublic = false
|
||||||
/** The user's real name */
|
|
||||||
realName = ""
|
|
||||||
/** The ID of the continent on which the citizen is located */
|
/** The ID of the continent on which the citizen is located */
|
||||||
continentId = ""
|
continentId = ""
|
||||||
/** The area within that continent where the citizen is located */
|
/** The area within that continent where the citizen is located */
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<template v-if="errors.length > 0">
|
<template v-if="errors.length > 0">
|
||||||
<p>The following error<template v-if="errors.length !== 1">s</template> occurred:</p>
|
<p>The following error<template v-if="errors.length !== 1">s</template> occurred:</p>
|
||||||
<ul>
|
<ul><li v-for="(error, idx) in errors" :key="idx">{{error}}</li></ul>
|
||||||
<li v-for="(error, idx) in errors" :key="idx">{{error}}</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
</template>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<svg viewbox="0 0 24 24">
|
<svg viewbox="0 0 24 24"><path :fill="color || 'white'" :d="icon" /></svg>
|
||||||
<path :fill="color || 'white'" :d="icon" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
<button :class="previewClass" @click.prevent="showPreview">Preview</button>
|
<button :class="previewClass" @click.prevent="showPreview">Preview</button>
|
||||||
</nav>
|
</nav>
|
||||||
<section class="preview" v-if="preview" v-html="previewHtml" aria-label="Rendered Markdown preview" />
|
<section v-if="preview" class="preview" v-html="previewHtml" aria-label="Rendered Markdown preview" />
|
||||||
<div class="form-floating" v-else>
|
<div v-else class="form-floating">
|
||||||
<textarea :id="id" class="form-control md-edit" :class="{ 'is-invalid': isInvalid }" rows="10" v-text="text"
|
<textarea :id="id" class="form-control md-edit" :class="{ 'is-invalid': isInvalid }" rows="10" v-text="text"
|
||||||
@input="$emit('update:text', $event.target.value)"></textarea>
|
@input="$emit('update:text', $event.target.value)"></textarea>
|
||||||
<div class="invalid-feedback">Please enter some text for {{label}}</div>
|
<div class="invalid-feedback">Please enter some text for {{label}}</div>
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<router-link to="/listings/mine" @click="hide">
|
<router-link to="/listings/mine" @click="hide">
|
||||||
<icon :icon="mdiSignText" /> My Job Listings
|
<icon :icon="mdiSignText" /> My Job Listings
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/citizen/profile" @click="hide">
|
<router-link to="/profile/edit" @click="hide">
|
||||||
<icon :icon="mdiPencil" /> My Employment Profile
|
<icon :icon="mdiPencil" /> My Employment Profile
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="mobileMenu" class="offcanvas offcanvas-end" v-if="showMobileMenu" tabindex="-1"
|
<div v-if="showMobileMenu" id="mobileMenu" class="offcanvas offcanvas-end" tabindex="-1"
|
||||||
aria-labelledby="mobileMenuLabel">
|
aria-labelledby="mobileMenuLabel">
|
||||||
<div class="offcanvas-header">
|
<div class="offcanvas-header">
|
||||||
<h5 id="mobileMenuLabel">Menu</h5>
|
<h5 id="mobileMenuLabel">Menu</h5>
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="offcanvas-body"><app-links /></div>
|
<div class="offcanvas-body"><app-links /></div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="collapse show p-3" v-else>
|
<aside v-else class="collapse show p-3">
|
||||||
<p class="home-link pb-3"><router-link to="/">Jobs, Jobs, Jobs</router-link></p>
|
<p class="home-link pb-3"><router-link to="/">Jobs, Jobs, Jobs</router-link></p>
|
||||||
<p> </p>
|
<p> </p>
|
||||||
<app-links />
|
<app-links />
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<nav class="navbar navbar-dark" v-if="showMobileHeader">
|
<nav v-if="showMobileHeader" class="navbar navbar-dark">
|
||||||
<span class="navbar-text"><router-link to="/">Jobs, Jobs, Jobs</router-link></span>
|
<span class="navbar-text"><router-link to="/">Jobs, Jobs, Jobs</router-link></span>
|
||||||
<button class="btn" data-bs-toggle="offcanvas" data-bs-target="#mobileMenu" aria-controls="mobileMenu">
|
<button class="btn" data-bs-toggle="offcanvas" data-bs-target="#mobileMenu" aria-controls="mobileMenu">
|
||||||
<icon :icon="mdiMenu" />
|
<icon :icon="mdiMenu" />
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="navbar navbar-light bg-light" v-else>
|
<nav v-else class="navbar navbar-light bg-light">
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<span class="navbar-text">
|
<span class="navbar-text">
|
||||||
(…and Jobs – <audio-clip clip="pelosi-jobs">Let’s Vote for Jobs!</audio-clip>)
|
(…and Jobs – <audio-clip clip="pelosi-jobs">Let’s Vote for Jobs!</audio-clip>)
|
||||||
|
|
|
@ -79,24 +79,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
component: () => import(/* webpackChunkName: "logon" */ "../views/citizen/LogOn.vue"),
|
component: () => import(/* webpackChunkName: "logon" */ "../views/citizen/LogOn.vue"),
|
||||||
meta: { auth: false, title: "Log On" }
|
meta: { auth: false, title: "Log On" }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/citizen/:abbr/authorized",
|
|
||||||
name: "CitizenAuthorized",
|
|
||||||
component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Authorized.vue"),
|
|
||||||
meta: { title: "Logging On" }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/citizen/dashboard",
|
path: "/citizen/dashboard",
|
||||||
name: "Dashboard",
|
name: "Dashboard",
|
||||||
component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Dashboard.vue"),
|
component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Dashboard.vue"),
|
||||||
meta: { auth: true, title: "Dashboard" }
|
meta: { auth: true, title: "Dashboard" }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/citizen/profile",
|
|
||||||
name: "EditProfile",
|
|
||||||
component: () => import(/* webpackChunkName: "profedit" */ "../views/citizen/EditProfile.vue"),
|
|
||||||
meta: { auth: true, title: "Edit Profile" }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/citizen/log-off",
|
path: "/citizen/log-off",
|
||||||
name: "LogOff",
|
name: "LogOff",
|
||||||
|
@ -141,6 +129,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
component: () => import(/* webpackChunkName: "profview" */ "../views/profile/ProfileView.vue"),
|
component: () => import(/* webpackChunkName: "profview" */ "../views/profile/ProfileView.vue"),
|
||||||
meta: { auth: true, title: "Loading Profile..." }
|
meta: { auth: true, title: "Loading Profile..." }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/profile/edit",
|
||||||
|
name: "EditProfile",
|
||||||
|
component: () => import(/* webpackChunkName: "profedit" */ "../views/profile/EditProfile.vue"),
|
||||||
|
meta: { auth: true, title: "Edit Profile" }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/profile/search",
|
path: "/profile/search",
|
||||||
name: "SearchProfiles",
|
name: "SearchProfiles",
|
||||||
|
|
|
@ -3,6 +3,3 @@ export const LogOn = "logOn"
|
||||||
|
|
||||||
/** Ensures that the continent list in the state has been populated */
|
/** Ensures that the continent list in the state has been populated */
|
||||||
export const EnsureContinents = "ensureContinents"
|
export const EnsureContinents = "ensureContinents"
|
||||||
|
|
||||||
/** Ensures that the Mastodon instance list in the state has been populated */
|
|
||||||
export const EnsureInstances = "ensureInstances"
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useTitle } from "@vueuse/core"
|
import { useTitle } from "@vueuse/core"
|
||||||
import { InjectionKey } from "vue"
|
import { InjectionKey } from "vue"
|
||||||
import { createStore, Store, useStore as baseUseStore } from "vuex"
|
import { createStore, Store, useStore as baseUseStore } from "vuex"
|
||||||
import api, { Continent, Instance, LogOnSuccess } from "../api"
|
import api, { Continent, LogOnSuccess } from "../api"
|
||||||
import * as Actions from "./actions"
|
import * as Actions from "./actions"
|
||||||
import * as Mutations from "./mutations"
|
import * as Mutations from "./mutations"
|
||||||
|
|
||||||
|
@ -15,8 +15,6 @@ export interface State {
|
||||||
logOnState : string
|
logOnState : string
|
||||||
/** All continents (use `ensureContinents` action) */
|
/** All continents (use `ensureContinents` action) */
|
||||||
continents : Continent[]
|
continents : Continent[]
|
||||||
/** All instances (use `ensureInstances` action) */
|
|
||||||
instances : Instance[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An injection key to identify this state with Vue */
|
/** An injection key to identify this state with Vue */
|
||||||
|
@ -35,9 +33,8 @@ export default createStore({
|
||||||
return {
|
return {
|
||||||
pageTitle: "",
|
pageTitle: "",
|
||||||
user: undefined,
|
user: undefined,
|
||||||
logOnState: "<em>Welcome back!</em>",
|
logOnState: "",
|
||||||
continents: [],
|
continents: []
|
||||||
instances: []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
@ -48,15 +45,15 @@ export default createStore({
|
||||||
[Mutations.SetUser]: (state, user : LogOnSuccess) => { state.user = user },
|
[Mutations.SetUser]: (state, user : LogOnSuccess) => { state.user = user },
|
||||||
[Mutations.ClearUser]: (state) => { state.user = undefined },
|
[Mutations.ClearUser]: (state) => { state.user = undefined },
|
||||||
[Mutations.SetLogOnState]: (state, message : string) => { state.logOnState = message },
|
[Mutations.SetLogOnState]: (state, message : string) => { state.logOnState = message },
|
||||||
[Mutations.SetContinents]: (state, continents : Continent[]) => { state.continents = continents },
|
[Mutations.SetContinents]: (state, continents : Continent[]) => { state.continents = continents }
|
||||||
[Mutations.SetInstances]: (state, instances : Instance[]) => { state.instances = instances }
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
[Actions.LogOn]: async ({ commit }, { abbr, code }) => {
|
[Actions.LogOn]: async ({ commit }, { form }) => {
|
||||||
const logOnResult = await api.citizen.logOn(abbr, code)
|
const logOnResult = await api.citizen.logOn(form)
|
||||||
if (typeof logOnResult === "string") {
|
if (typeof logOnResult === "string") {
|
||||||
commit(Mutations.SetLogOnState, logOnResult)
|
commit(Mutations.SetLogOnState, logOnResult)
|
||||||
} else {
|
} else {
|
||||||
|
commit(Mutations.SetLogOnState, "")
|
||||||
commit(Mutations.SetUser, logOnResult)
|
commit(Mutations.SetUser, logOnResult)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -68,17 +65,6 @@ export default createStore({
|
||||||
} else {
|
} else {
|
||||||
commit(Mutations.SetContinents, theSeven)
|
commit(Mutations.SetContinents, theSeven)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[Actions.EnsureInstances]: async ({ state, commit }) => {
|
|
||||||
if (state.instances.length > 0) return
|
|
||||||
const instResp = await api.instances.all()
|
|
||||||
if (typeof instResp === "string") {
|
|
||||||
console.error(instResp)
|
|
||||||
} else if (typeof instResp === "undefined") {
|
|
||||||
console.error("No instances were found; this should not happen")
|
|
||||||
} else {
|
|
||||||
commit(Mutations.SetInstances, instResp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
|
|
|
@ -12,6 +12,3 @@ export const SetLogOnState = "setLogOnState"
|
||||||
|
|
||||||
/** Set the list of continents */
|
/** Set the list of continents */
|
||||||
export const SetContinents = "setContinents"
|
export const SetContinents = "setContinents"
|
||||||
|
|
||||||
/** Set the list of Mastodon instances */
|
|
||||||
export const SetInstances = "setInstances"
|
|
||||||
|
|
|
@ -1,161 +1,197 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3 How It Works
|
<h3>How It Works</h3>
|
||||||
h5.pb-3.text-muted: em Last Updated August 29#[sup th], 2021
|
<h5 class="pb-3 text-muted fst-italic">Last Updated August 29<sup>th</sup>, 2021</h5>
|
||||||
p: em.
|
<p class="fst-italic">
|
||||||
Show me how to #[a(href="#listing-search") find a job]
|
Show me how to <a href="#listing-search">find a job</a>
|
||||||
#[!= " • "]#[a(href="#listing") list a job opportunity]
|
• <a href="#listing">list a job opportunity</a>
|
||||||
#[!= " • "]#[a(href="#profile-search") find people to hire]
|
• <a href="#profile-search">find people to hire</a>
|
||||||
#[!= " • "]#[a(href="#profile") create an employment profile]
|
• <a href="#profile">create an employment profile</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
hr
|
<hr>
|
||||||
|
|
||||||
h4#listing-search Find a Job Listing
|
<h4 id="listing-search">Find a Job Listing</h4>
|
||||||
p.
|
<p>
|
||||||
Active job listings are found on the #[span.link Help Wanted!] page. When you first bring up this page, you will see
|
Active job listings are found on the <ref-page>Help Wanted!</ref-page> page. When you first bring up this page,
|
||||||
several criteria by which you can narrow your results, though none are required. When you click the
|
you will see several criteria by which you can narrow your results, though none are required. When you click the
|
||||||
#[span.button Search] button, you will see open job listings filtered by whatever criteria you specified. Each job
|
<ref-button>Search</ref-button> button, you will see open job listings filtered by whatever criteria you
|
||||||
displays its title, its location, whether it is a remote opportunity, and (if specified) the date by which the job
|
specified. Each job displays its title, its location, whether it is a remote opportunity, and (if specified) the
|
||||||
needs to be filled.
|
date by which the job needs to be filled.
|
||||||
p.
|
</p>
|
||||||
Clicking the #[span.link View] link on a listing brings up the full view page for a listing. This page displays all
|
<p>
|
||||||
of the information from the search results, along with the citizen who posted it, and the full details of the job.
|
Clicking the <ref-page>View</ref-page> link on a listing brings up the full view page for a listing. This page
|
||||||
The citizen’s name is a link to their profile page at their Mastodon instance; you can use that to get their
|
displays all of the information from the search results, along with the citizen who posted it, and the full
|
||||||
handle, and use Mastodon’s communication facilites to inquire about the position.
|
details of the job. The citizen’s name is a link to their profile page at their Mastodon instance; you can
|
||||||
p: em.text-muted.
|
use that to get their handle, and use Mastodon’s communication facilites to inquire about the position.
|
||||||
|
</p>
|
||||||
|
<p class="fst-italic text-muted">
|
||||||
(If you know of a way to construct a link to Mastodon that would start a direct message, please reach out;
|
(If you know of a way to construct a link to Mastodon that would start a direct message, please reach out;
|
||||||
I’ve searched and searched, and asked NAS, but have not yet determined how to do that.)
|
I’ve searched and searched, and asked NAS, but have not yet determined how to do that.)
|
||||||
|
</p>
|
||||||
|
|
||||||
hr
|
<hr>
|
||||||
|
|
||||||
h4#listing Job Listings
|
<h4 id="listing">Job Listings</h4>
|
||||||
h5 Create a Job Listing
|
<h5>Create a Job Listing</h5>
|
||||||
p.
|
<p>
|
||||||
The #[span.link My Job Listings] page shows all of the job listings you have created. To add a new one, click the
|
The <ref-page>My Job Listings</ref-page> page shows all of the job listings you have created. To add a new one,
|
||||||
#[span.button Add a New Listing] button. This page allows you to specify a title for the listing; the continent and
|
click the <ref-button>Add a New Listing</ref-button> button. This page allows you to specify a title for the
|
||||||
region; whether it is a remote opportunity; the date by which a job needs to be filled; and a full description of
|
listing; the continent and region; whether it is a remote opportunity; the date by which a job needs to be filled;
|
||||||
the position, using #[a(href="#markdown") Markdown]. Once you save the listing, it will be visible to the other
|
and a full description of the position, using <a href="#markdown">Markdown</a>. Once you save the listing, it will
|
||||||
citizens here.
|
be visible to the other citizens here.
|
||||||
|
</p>
|
||||||
|
|
||||||
h5 Maintain and Share Your Job Listings
|
<h5>Maintain and Share Your Job Listings</h5>
|
||||||
p.
|
<p>
|
||||||
The #[span.link My Job Listings] page will show you all of your active job listings just below the
|
The <ref-page>My Job Listings</ref-page> 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
|
<ref-button>Add a Job Listing</ref-button> button. Within this table, you can edit the listing, view it, or expire
|
||||||
that below). The #[span.link View] link will show you the job listing just as other users will see it. You can share
|
it (more on that below). The <ref-page>View</ref-page> link will show you the job listing just as other users will
|
||||||
the link from your browser on any No Agenda-affiliated Mastodon instance, and those who click on it will be able to
|
see it. You can share the link from your browser on any No Agenda-affiliated Mastodon instance, and those who
|
||||||
view it. (Existing users of Jobs, Jobs, Jobs will go right to it; others will need to authorize this site’s
|
click on it will be able to view it. (Existing users of Jobs, Jobs, Jobs will go right to it; others will need to
|
||||||
access, but then they will get there as well.)
|
authorize this site’s access, but then they will get there as well.)
|
||||||
|
</p>
|
||||||
|
|
||||||
h5 Expire a Job Listing
|
<h5>Expire a Job Listing</h5>
|
||||||
p.
|
<p>
|
||||||
Once the job is filled, or the opportunity has passed, you will want to expire the listing; this is what the
|
Once the job is filled, or the opportunity has passed, you will want to expire the listing; this is what the
|
||||||
#[span.link Expire] link allows you to do. When you click it, you will be presented with a single question –
|
<ref-page>Expire</ref-page> link allows you to do. When you click it, you will be presented with a single question
|
||||||
was the job filled due to its listing here? If not, leave that blank, click the #[span.button Expire] button, and
|
– was the job filled due to its listing here? If not, leave that blank, click the
|
||||||
the listing will be expired. If you click that box, though, another Markdown editor will appear, where you can share
|
<ref-button>Expire</ref-button> button, and the listing will be expired. If you click that box, though, another
|
||||||
a story of the experience. This is not required, but if you put text there, it will be recorded as a Success Story,
|
Markdown editor will appear, where you can share a story of the experience. This is not required, but if you put
|
||||||
and other users will be able to read about your success.
|
text there, it will be recorded as a Success Story, and other users will be able to read about your success.
|
||||||
p.
|
</p>
|
||||||
Once you have at least one expired job listing, the #[span.link My Job Listing] page will have a new section below
|
<p>
|
||||||
your active listings, where you can see your expired ones. You can still view the expired listing, and links that
|
Once you have at least one expired job listing, the <ref-page>My Job Listing</ref-page> page will have a new
|
||||||
you may have shared will still pull up the listing; there will be an “expired” label beside the title,
|
section below your active listings, where you can see your expired ones. You can still view the expired listing,
|
||||||
so that whoever is viewing it knows that they are reading about a job that is no longer available.
|
and links that you may have shared will still pull up the listing; there will be an “expired” label
|
||||||
|
beside the title, so that whoever is viewing it knows that they are reading about a job that is no longer
|
||||||
|
available.
|
||||||
|
</p>
|
||||||
|
|
||||||
hr
|
<hr>
|
||||||
|
|
||||||
h4#profile-search Searching Profiles
|
<h4 id="profile-search">Searching Profiles</h4>
|
||||||
p.
|
<p>
|
||||||
The #[span.link Employment Profiles] link at the side allows you to search for profiles by continent, the
|
The <ref-page>Employment Profiles</ref-page> link at the side allows you to search for profiles by continent, the
|
||||||
citizen’s desire for remote work, a skill, or any text in their professional biography and experience. If you
|
citizen’s desire for remote work, a skill, or any text in their professional biography and experience. If
|
||||||
find someone with whom you’d like to discuss potential opportunities, the name at the top of the profile links
|
you find someone with whom you’d like to discuss potential opportunities, the name at the top of the profile
|
||||||
to their Mastodon profile, where you can use its features to get in touch.
|
links to their Mastodon profile, where you can use its features to get in touch.
|
||||||
|
</p>
|
||||||
|
|
||||||
hr
|
<hr>
|
||||||
|
|
||||||
h4#profile Your Employment Profile
|
<h4 id="profile">Your Employment Profile</h4>
|
||||||
p.
|
<p>
|
||||||
The employment profile is your résumé, visible to other citizens here. It also allows you to specify
|
The employment profile is your résumé, 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,
|
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 your Mastodon instance; this system updates your
|
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.
|
current display name each time you log on.
|
||||||
|
</p>
|
||||||
|
|
||||||
h5 Completing Your Profile
|
<h5>Completing Your Profile</h5>
|
||||||
p.
|
<p>
|
||||||
The #[span.link My Employment Profile] page lets you establish or modify your employment profile; the
|
The <ref-page>My Employment Profile</ref-page> page lets you establish or modify your employment profile; the
|
||||||
#[span.link Dashboard] page also has buttons that let you create, edit, and view your profile.
|
<ref-page>Dashboard</ref-page> page also has buttons that let you create, edit, and view your profile.
|
||||||
ul
|
</p>
|
||||||
li.
|
<ul>
|
||||||
The #[span.link Professional Biography] is the “Objective” part of a traditional résumé.
|
<li>
|
||||||
This section supports #[a(href="#markdown") Markdown], so you can include actual headings, formatting, etc.
|
The <ref-page>Professional Biography</ref-page> is the “Objective” part of a traditional
|
||||||
li.
|
résumé. This section supports <a href="#markdown">Markdown</a>, so you can include actual
|
||||||
|
headings, formatting, etc.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
Skills are optional, but they are the place to record skills you have. Along with each skill, there is a
|
Skills are optional, but they are the place to record skills you have. Along with each skill, there is a
|
||||||
#[span.link Notes] field, which can be used to indicate the time you’ve practiced a particular skill, the
|
<ref-page>Notes</ref-page> field, which can be used to indicate the time you’ve practiced a particular
|
||||||
mastery you have of that skill, etc. It is free-form text, so it is all up to you how you utilize the field.
|
skill, the mastery you have of that skill, etc. It is free-form text, so it is all up to you how you utilize
|
||||||
li.
|
the field.
|
||||||
The #[span.link Experience] field is intended to capture a chronological or topical employment history. This
|
</li>
|
||||||
Markdown space can be used to capture chronological history, certifications, or any other information –
|
<li>
|
||||||
however you would like it presented to fellow citizens.
|
The <ref-page>Experience</ref-page> field is intended to capture a chronological or topical employment history.
|
||||||
#[em.text-muted (If you would like a chronological job builder, reach out and let us know.)]
|
This Markdown space can be used to capture chronological history, certifications, or any other information
|
||||||
li.
|
– however you would like it presented to fellow citizens.
|
||||||
If you check the #[span.link Allow my profile to be searched publicly] checkbox #[strong and] you are seeking
|
<em class="text-muted">(If you would like a chronological job builder, reach out and let us know.)</em>
|
||||||
employment, your continent, region, and skills fields will be searchable and displayed to public users of the
|
</li>
|
||||||
site. They will not be tied to your Mastodon handle or real name; they are there to let people peek behind the
|
<li>
|
||||||
curtain a bit, and hopefully inspire them to join us.
|
If you check the <ref-page>Allow my profile to be searched publicly</ref-page> checkbox <strong>and</strong> 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 Mastodon handle or real name; they are there to let people peek
|
||||||
|
behind the curtain a bit, and hopefully inspire them to join us.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
h5 Viewing and Sharing Your Profile
|
<h5>Viewing and Sharing Your Profile</h5>
|
||||||
p.
|
<p>
|
||||||
Once your profile has been established, the #[span.link My Employment Profile] page will have a button at the bottom
|
Once your profile has been established, the <ref-page>My Employment Profile</ref-page> page will have a button at
|
||||||
that will let you view your profile the way all other validated users will be able to see it. (There will also be a
|
the bottom that will let you view your profile the way all other validated users will be able to see it. (There
|
||||||
link to this page from the #[span.link Dashboard].) The URL of this page can be shared on any No Agenda-affiliated
|
will also be a link to this page from the <ref-page>Dashboard</ref-page>.) The URL of this page can be shared on
|
||||||
Mastodon instance, if you would like to share it there. Just as with job listings, existing users will go straight
|
any No Agenda-affiliated Mastodon instance, if you would like to share it there. Just as with job listings,
|
||||||
there, while others will get there once they authorize this application.
|
existing users will go straight there, while others will get there once they authorize this application.
|
||||||
p.
|
</p>
|
||||||
|
<p>
|
||||||
The name on employment profiles is a link to that user’s profile on their Mastodon instance; from there,
|
The name on employment profiles is a link to that user’s profile on their Mastodon instance; from there,
|
||||||
others can communicate further with you using the tools Mastodon provides.
|
others can communicate further with you using the tools Mastodon provides.
|
||||||
|
</p>
|
||||||
|
|
||||||
h5 “I Found a Job!”
|
<h5>“I Found a Job!”</h5>
|
||||||
p.
|
<p>
|
||||||
If your profile indicates that you are seeking employment, and you secure employment, that is something you will
|
If your profile indicates that you are seeking employment, and you secure employment, that is something you will
|
||||||
want to update (and – congratulations!). From both the #[span.link Dashboard] and
|
want to update (and – congratulations!). From both the <ref-page>Dashboard</ref-page> and
|
||||||
#[span.link My Employment Profile] pages, you will see a link that encourages you to tell us about it. Click either
|
<ref-page>My Employment Profile</ref-page> pages, you will see a link that encourages you to tell us about it.
|
||||||
of those links, and you will be brought to a page that allows you to indicate whether your employment actually came
|
Click either of those links, and you will be brought to a page that allows you to indicate whether your employment
|
||||||
from someone finding your profile on Jobs, Jobs, Jobs, and gives you a place to write about the experience. These
|
actually came from someone finding your profile on Jobs, Jobs, Jobs, and gives you a place to write about the
|
||||||
stories are only viewable by validated users, so feel free to use as much (or as little) identifying information as
|
experience. These stories are only viewable by validated users, so feel free to use as much (or as little)
|
||||||
you’d like. You can also submit this page with all the fields blank; in that case, your “Seeking
|
identifying information as you’d like. You can also submit this page with all the fields blank; in that
|
||||||
Employment” flag is cleared, and the blank story is recorded.
|
case, your “Seeking Employment” flag is cleared, and the blank story is recorded.
|
||||||
p.
|
</p>
|
||||||
As a validated user, you can also view others success stories. Clicking #[span.link Success Stories] in the sidebar
|
<p>
|
||||||
will display a list of all the stories that have been recorded. If there is a story to be read, there will be a link
|
As a validated user, you can also view others success stories. Clicking <ref-page>Success Stories</ref-page> in
|
||||||
to read it; if you submitted the story, there will also be an #[span.link Edit] link.
|
the sidebar will display a list of all the stories that have been recorded. If there is a story to be read, there
|
||||||
|
will be a link to read it; if you submitted the story, there will also be an <ref-page>Edit</ref-page> link.
|
||||||
|
</p>
|
||||||
|
|
||||||
h5 Publicly Available Information
|
<h5>Publicly Available Information</h5>
|
||||||
p.
|
<p>
|
||||||
The #[span.link Job Seekers] page for profile information will allow users to search for and display the continent,
|
The <ref-page>Job Seekers</ref-page> page for profile information will allow users to search for and display the
|
||||||
region, skills, and notes of users who are seeking employment #[strong and] have opted in to their information being
|
continent, region, skills, and notes of users who are seeking employment <strong>and</strong> have opted in to
|
||||||
publicly searchable. If you are a public user, this information is always the latest we have; check out the link at
|
their information being publicly searchable. If you are a public user, this information is always the latest we
|
||||||
the top of the search results for how you can learn more about these fine human resources!
|
have; check out the link at the top of the search results for how you can learn more about these fine human
|
||||||
|
resources!
|
||||||
|
</p>
|
||||||
|
|
||||||
hr
|
<hr>
|
||||||
|
|
||||||
h4#markdown A Bit about Markdown
|
<h4 id="markdown">A Bit about Markdown</h4>
|
||||||
p.
|
<p>
|
||||||
Markdown is a plain-text way to specify formatting quite similar to that provided by word processors. The
|
Markdown is a plain-text way to specify formatting quite similar to that provided by word processors. The
|
||||||
#[a(href="https://daringfireball.net/projects/markdown/" target="_blank") original page] for the project is a good
|
<a href="https://daringfireball.net/projects/markdown/" target="_blank" rel="noopener">original page</a> for the
|
||||||
good overview of its capabilities, and the pages at
|
project is a good good overview of its capabilities, and the pages at
|
||||||
#[a(href="https://www.markdownguide.org/" target="_blank") Markdown Guide] give in-depth lessons to make the most of
|
<a href="https://www.markdownguide.org/" target="_blank" rel="noopener">Markdown Guide</a> give in-depth lessons
|
||||||
this language. The version of Markdown employed here supports many popular extensions, include smart quotes (turning
|
to make the most of this language. The version of Markdown employed here supports many popular extensions, include
|
||||||
"a quote" into “a quote”), tables, super/subscripts, and more.
|
smart quotes (turning "a quote" into “a quote”), tables, super/subscripts, and more.
|
||||||
|
</p>
|
||||||
|
|
||||||
hr
|
<hr>
|
||||||
|
|
||||||
h4 Help / Suggestions
|
<h4>Help / Suggestions</h4>
|
||||||
p.
|
<p>
|
||||||
This is open-source software
|
This is open-source software
|
||||||
#[a(href="https://github.com/bit-badger/jobs-jobs-jobs" _target="_blank") developed on Github]; feel free to
|
<a href="https://github.com/bit-badger/jobs-jobs-jobs" _target="_blank" rel="noopener">developed on Github</a>];
|
||||||
#[a(href="https://github.com/bit-badger/jobs-jobs-jobs/issues" target="_blank") create an issue there], or look up
|
feel free to <a href="https://github.com/bit-badger/jobs-jobs-jobs/issues" target="_blank" rel="noopener">create
|
||||||
@danieljsummers on No Agenda Social.
|
an issue there</a>, or look up @danieljsummers on No Agenda Social.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
|
ref-page
|
||||||
|
background-color: rgba(144, 238, 144, .25)
|
||||||
|
ref-button
|
||||||
|
border: solid 1px lightgreen
|
||||||
|
border-radius: .25rem
|
||||||
|
ref-page,
|
||||||
|
ref-button
|
||||||
|
padding: 0 .25rem
|
||||||
span.link
|
span.link
|
||||||
background-color: rgba(144, 238, 144, .25)
|
background-color: rgba(144, 238, 144, .25)
|
||||||
span.button
|
span.button
|
||||||
|
|
|
@ -1,345 +1,427 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3 Privacy Policy
|
<h3>Privacy Policy</h3>
|
||||||
p: em (as of September 6#[sup th], 2021)
|
<p class="fst-italic">(as of August 30<sup>th</sup>, 2021)</p>
|
||||||
|
|
||||||
p.
|
<p>
|
||||||
{{name}} (“we,” “our,” or “us”) is committed to protecting your privacy. This
|
{{name}} (“we,” “our,” or “us”) is committed to protecting your privacy. This
|
||||||
Privacy Policy explains how your personal information is collected, used, and disclosed by {{name}}.
|
Privacy Policy explains how your personal information is collected, used, and disclosed by {{name}}.
|
||||||
p.
|
</p>
|
||||||
This Privacy Policy applies to our website, and its associated subdomains (collectively, our “Service”)
|
<p>
|
||||||
alongside our application, {{name}}. By accessing or using our Service, you signify that you have read, understood,
|
This Privacy Policy applies to our website, and its associated subdomains (collectively, our
|
||||||
and agree to our collection, storage, use, and disclosure of your personal information as described in this Privacy
|
“Service”) alongside our application, {{name}}. By accessing or using our Service, you signify that
|
||||||
Policy and our Terms of Service.
|
you have read, understood, and agree to our collection, storage, use, and disclosure of your personal information
|
||||||
|
as described in this Privacy Policy and our Terms of Service.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Definitions and key terms
|
<h4>Definitions and key terms</h4>
|
||||||
p.
|
<p>
|
||||||
To help explain things as clearly as possible in this Privacy Policy, every time any of these terms are referenced,
|
To help explain things as clearly as possible in this Privacy Policy, every time any of these terms are referenced,
|
||||||
are strictly defined as:
|
are strictly defined as:
|
||||||
ul
|
</p>
|
||||||
li.
|
<ul>
|
||||||
|
<li>
|
||||||
Cookie: small amount of data generated by a website and saved by your web browser. It is used to identify your
|
Cookie: small amount of data generated by a website and saved by your web browser. It is used to identify your
|
||||||
browser, provide analytics, remember information about you such as your language preference or login information.
|
browser, provide analytics, remember information about you such as your language preference or login
|
||||||
li.
|
information.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
Company: when this policy mentions “Company,” “we,” “us,” or
|
Company: when this policy mentions “Company,” “we,” “us,” or
|
||||||
“our,” it refers to {{name}}, that is responsible for your information under this Privacy Policy.
|
“our,” it refers to {{name}}, that is responsible for your information under this Privacy Policy.
|
||||||
li Country: where {{name}} or the owners/founders of {{name}} are based, in this case is US.
|
</li>
|
||||||
li.
|
<li>Country: where {{name}} or the owners/founders of {{name}} are based, in this case is US.</li>
|
||||||
|
<li>
|
||||||
Customer: refers to the company, organization or person that signs up to use the {{name}} Service to manage the
|
Customer: refers to the company, organization or person that signs up to use the {{name}} Service to manage the
|
||||||
relationships with your consumers or service users.
|
relationships with your consumers or service users.
|
||||||
li.
|
</li>
|
||||||
|
<li>
|
||||||
Device: any internet connected device such as a phone, tablet, computer or any other device that can be used to
|
Device: any internet connected device such as a phone, tablet, computer or any other device that can be used to
|
||||||
visit {{name}} and use the services.
|
visit {{name}} and use the services.
|
||||||
li.
|
</li>
|
||||||
|
<li>
|
||||||
IP address: Every device connected to the Internet is assigned a number known as an Internet protocol (IP)
|
IP address: Every device connected to the Internet is assigned a number known as an Internet protocol (IP)
|
||||||
address. These numbers are usually assigned in geographic blocks. An IP address can often be used to identify the
|
address. These numbers are usually assigned in geographic blocks. An IP address can often be used to identify
|
||||||
location from which a device is connecting to the Internet.
|
the location from which a device is connecting to the Internet.
|
||||||
li.
|
</li>
|
||||||
Personnel: refers to those individuals who are employed by {{name}} or are under contract to perform a service on
|
<li>
|
||||||
behalf of one of the parties.
|
Personnel: refers to those individuals who are employed by {{name}} or are under contract to perform a service
|
||||||
li.
|
on behalf of one of the parties.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
Personal Data: any information that directly, indirectly, or in connection with other information — including a
|
Personal Data: any information that directly, indirectly, or in connection with other information — including a
|
||||||
personal identification number — allows for the identification or identifiability of a natural person.
|
personal identification number — allows for the identification or identifiability of a natural person.
|
||||||
li.
|
</li>
|
||||||
Service: refers to the service provided by {{name}} as described in the relative terms (if available) and on this
|
<li>
|
||||||
platform.
|
Service: refers to the service provided by {{name}} as described in the relative terms (if available) and on
|
||||||
li.
|
this platform.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
Third-party service: refers to advertisers, contest sponsors, promotional and marketing partners, and others who
|
Third-party service: refers to advertisers, contest sponsors, promotional and marketing partners, and others who
|
||||||
provide our content or whose products or services we think may interest you.
|
provide our content or whose products or services we think may interest you.
|
||||||
li.
|
</li>
|
||||||
|
<li>
|
||||||
Website: {{name}}’s site, which can be accessed via this URL:
|
Website: {{name}}’s site, which can be accessed via this URL:
|
||||||
#[router-link(to="/") https://noagendacareers.com/]
|
<router-link to="/">https://noagendacareers.com/</router-link>
|
||||||
li You: a person or entity that is registered with {{name}} to use the Services.
|
</li>
|
||||||
|
<li>You: a person or entity that is registered with {{name}} to use the Services.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
h4 What Information Do We Collect?
|
<h4>What Information Do We Collect?</h4>
|
||||||
p We collect information from you when you visit our website, register on our site, or fill out a form.
|
<p>We collect information from you when you visit our website, register on our site, or fill out a form.</p>
|
||||||
ul
|
<ul>
|
||||||
li Name / Username
|
<li>Name / Username</li>
|
||||||
li Coarse Geographic Location
|
<li>Coarse Geographic Location</li>
|
||||||
li Employment History
|
<li>Employment History</li>
|
||||||
li Mastodon Account Name / Profile
|
<li>Job Listing Information</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
h4 How Do We Use The Information We Collect?
|
<h4>How Do We Use The Information We Collect?</h4>
|
||||||
p Any of the information we collect from you may be used in one of the following ways:
|
<p>Any of the information we collect from you may be used in one of the following ways:</p>
|
||||||
ul
|
<ul>
|
||||||
li To personalize your experience (your information helps us to better respond to your individual needs)
|
<li>To personalize your experience (your information helps us to better respond to your individual needs)</li>
|
||||||
li.
|
<li>
|
||||||
To improve our website (we continually strive to improve our website offerings based on the information and
|
To improve our website (we continually strive to improve our website offerings based on the information and
|
||||||
feedback we receive from you)
|
feedback we receive from you)
|
||||||
li.
|
</li>
|
||||||
|
<li>
|
||||||
To improve customer service (your information helps us to more effectively respond to your customer service
|
To improve customer service (your information helps us to more effectively respond to your customer service
|
||||||
requests and support needs)
|
requests and support needs)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
h4 When does {{name}} use end user information from third parties?
|
<h4>When does {{name}} use end user information from third parties?</h4>
|
||||||
p {{name}} will collect End User Data necessary to provide the {{name}} services to our customers.
|
<p>{{name}} will collect End User Data necessary to provide the {{name}} services to our customers.</p>
|
||||||
p.
|
<p>
|
||||||
End users may voluntarily provide us with information they have made available on social media websites
|
End users may voluntarily provide us with information they have made available on social media websites. If you
|
||||||
(specifically No Agenda-affiliated Mastodon instances). If you provide us with any such information, we may collect
|
provide us with any such information, we may collect publicly available information from the social media websites
|
||||||
publicly available information from the social media websites you have indicated. You can control how much of your
|
you have indicated. You can control how much of your information social media websites make public by visiting
|
||||||
information social media websites make public by visiting these websites and changing your privacy settings.
|
these websites and changing your privacy settings.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 When does {{name}} use customer information from third parties?
|
<h4>When does {{name}} use customer information from third parties?</h4>
|
||||||
p We do not utilize third party information apart from the end-user data described above.
|
<p>We do not utilize third party information apart from the end-user data described above.</p>
|
||||||
|
|
||||||
h4 Do we share the information we collect with third parties?
|
<h4>Do we share the information we collect with third parties?</h4>
|
||||||
p.
|
<p>
|
||||||
We may disclose personal and non-personal information about you to government or law enforcement officials or
|
We may disclose personal and non-personal information about you to government or law enforcement officials or
|
||||||
private parties as we, in our sole discretion, believe necessary or appropriate in order to respond to claims, legal
|
private parties as we, in our sole discretion, believe necessary or appropriate in order to respond to claims,
|
||||||
process (including subpoenas), to protect our rights and interests or those of a third party, the safety of the
|
legal process (including subpoenas), to protect our rights and interests or those of a third party, the safety of
|
||||||
public or any person, to prevent or stop any illegal, unethical, or legally actionable activity, or to otherwise
|
the public or any person, to prevent or stop any illegal, unethical, or legally actionable activity, or to
|
||||||
comply with applicable court orders, laws, rules and regulations.
|
otherwise comply with applicable court orders, laws, rules and regulations.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Where and when is information collected from customers and end users?
|
<h4>Where and when is information collected from customers and end users?</h4>
|
||||||
p.
|
<p>
|
||||||
{{name}} will collect personal information that you submit to us. We may also receive personal information about you
|
{{name}} will collect personal information that you submit to us. We may also receive personal information about you
|
||||||
from third parties as described above.
|
from third parties as described above.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 How Do We Use Your E-mail Address?
|
<h4>How Do We Use Your E-mail Address?</h4>
|
||||||
p.
|
<p>
|
||||||
We do not collect nor use an e-mail address. If you have provided it in the free text areas of the site, other
|
{{name}} uses your e-mail address to identify you, along with your password, as an authorized user of this site.
|
||||||
validated users may be able to view it, but {{name}} does not search for nor utilize e-mail addresses from those
|
E-mail addresses are verified via a time-sensitive link, and may also be used to send password reset authorization
|
||||||
areas.
|
codes. We do not display this e-mail address to users. If you choose to add an e-mail address as a contact type,
|
||||||
|
that e-mail address will be visible to other authorized users.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 How Long Do We Keep Your Information?
|
<h4>How Long Do We Keep Your Information?</h4>
|
||||||
p.
|
<p>
|
||||||
We keep your information only so long as we need it to provide {{name}} to you and fulfill the purposes described in
|
We keep your information only so long as we need it to provide {{name}} to you and fulfill the purposes described
|
||||||
this policy. When we no longer need to use your information and there is no need for us to keep it to comply with
|
in this policy. When we no longer need to use your information and there is no need for us to keep it to comply
|
||||||
our legal or regulatory obligations, we’ll either remove it from our systems or depersonalize it so that we
|
with our legal or regulatory obligations, we’ll either remove it from our systems or depersonalize it so that we
|
||||||
can’t identify you.
|
can’t identify you.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 How Do We Protect Your Information?
|
<h4>How Do We Protect Your Information?</h4>
|
||||||
p.
|
<p>
|
||||||
We implement a variety of security measures to maintain the safety of your personal information when you enter,
|
We implement a variety of security measures to maintain the safety of your personal information when you enter,
|
||||||
submit, or access your personal information. We mandate the use of a secure server. We cannot, however, ensure or
|
submit, or access your personal information. We mandate the use of a secure server. We cannot, however, ensure or
|
||||||
warrant the absolute security of any information you transmit to {{name}} or guarantee that your information on the
|
warrant the absolute security of any information you transmit to {{name}} or guarantee that your information on
|
||||||
Service may not be accessed, disclosed, altered, or destroyed by a breach of any of our physical, technical, or
|
the Service may not be accessed, disclosed, altered, or destroyed by a breach of any of our physical, technical,
|
||||||
managerial safeguards.
|
or managerial safeguards.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Could my information be transferred to other countries?
|
<h4>Could my information be transferred to other countries?</h4>
|
||||||
p.
|
<p>
|
||||||
{{name}} is hosted in the US. Information collected via our website may be viewed and hosted anywhere in the world,
|
{{name}} is hosted in the US. Information collected via our website may be viewed and hosted anywhere in the
|
||||||
including countries that may not have laws of general applicability regulating the use and transfer of such data. To
|
world, including countries that may not have laws of general applicability regulating the use and transfer of
|
||||||
the fullest extent allowed by applicable law, by using any of the above, you voluntarily consent to the trans-border
|
such data. To the fullest extent allowed by applicable law, by using any of the above, you voluntarily consent to
|
||||||
transfer and hosting of such information.
|
the trans-border transfer and hosting of such information.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Is the information collected through the {{name}} Service secure?
|
<h4>Is the information collected through the {{name}} Service secure?</h4>
|
||||||
p.
|
<p>
|
||||||
We take precautions to protect the security of your information. We have physical, electronic, and managerial
|
We take precautions to protect the security of your information. We have physical, electronic, and managerial
|
||||||
procedures to help safeguard, prevent unauthorized access, maintain data security, and correctly use your
|
procedures to help safeguard, prevent unauthorized access, maintain data security, and correctly use your
|
||||||
information. However, neither people nor security systems are foolproof, including encryption systems. In addition,
|
information. However, neither people nor security systems are foolproof, including encryption systems. In
|
||||||
people can commit intentional crimes, make mistakes, or fail to follow policies. Therefore, while we use reasonable
|
addition, people can commit intentional crimes, make mistakes, or fail to follow policies. Therefore, while we
|
||||||
efforts to protect your personal information, we cannot guarantee its absolute security. If applicable law imposes
|
use reasonable efforts to protect your personal information, we cannot guarantee its absolute security. If
|
||||||
any non-disclaimable duty to protect your personal information, you agree that intentional misconduct will be the
|
applicable law imposes any non-disclaimable duty to protect your personal information, you agree that intentional
|
||||||
standards used to measure our compliance with that duty.
|
misconduct will be the standards used to measure our compliance with that duty.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Can I update or correct my information?
|
<h4>Can I update or correct my information?</h4>
|
||||||
p.
|
<p>
|
||||||
The rights you have to request updates or corrections to the information {{name}} collects depend on your
|
The rights you have to request updates or corrections to the information {{name}} collects depend on your
|
||||||
relationship with {{name}}.
|
relationship with {{name}}.
|
||||||
p.
|
</p>
|
||||||
|
<p>
|
||||||
Customers have the right to request the restriction of certain uses and disclosures of personally identifiable
|
Customers have the right to request the restriction of certain uses and disclosures of personally identifiable
|
||||||
information as follows. You can contact us in order to (1) update or correct your personally identifiable
|
information as follows. You can contact us in order to (1) update or correct your personally identifiable
|
||||||
information, or (3) delete the personally identifiable information maintained about you on our systems (subject to
|
information, or (3) delete the personally identifiable information maintained about you on our systems (subject to
|
||||||
the following paragraph), by cancelling your account. Such updates, corrections, changes and deletions will have no
|
the following paragraph), by cancelling your account. Such updates, corrections, changes and deletions will have
|
||||||
effect on other information that we maintain in accordance with this Privacy Policy prior to such update,
|
no effect on other information that we maintain in accordance with this Privacy Policy prior to such update,
|
||||||
correction, change, or deletion. You are responsible for maintaining the secrecy of your unique password and account
|
correction, change, or deletion. You are responsible for maintaining the secrecy of your unique password and
|
||||||
information at all times.
|
account information at all times.
|
||||||
p.
|
</p>
|
||||||
|
<p>
|
||||||
{{name}} also provides ways for users to modify or remove the information we have collected from them from the
|
{{name}} also provides ways for users to modify or remove the information we have collected from them from the
|
||||||
application; these actions will have the same effect as contacting us to modify or remove data.
|
application; these actions will have the same effect as contacting us to modify or remove data.
|
||||||
p.
|
</p>
|
||||||
|
<p>
|
||||||
You should be aware that it is not technologically possible to remove each and every record of the information you
|
You should be aware that it is not technologically possible to remove each and every record of the information you
|
||||||
have provided to us from our system. The need to back up our systems to protect information from inadvertent loss
|
have provided to us from our system. The need to back up our systems to protect information from inadvertent loss
|
||||||
means that a copy of your information may exist in a non-erasable form that will be difficult or impossible for us
|
means that a copy of your information may exist in a non-erasable form that will be difficult or impossible for us
|
||||||
to locate. Promptly after receiving your request, all personal information stored in databases we actively use, and
|
to locate. Promptly after receiving your request, all personal information stored in databases we actively use,
|
||||||
other readily searchable media will be updated, corrected, changed, or deleted, as appropriate, as soon as and to
|
and other readily searchable media will be updated, corrected, changed, or deleted, as appropriate, as soon as and
|
||||||
the extent reasonably and technically practicable.
|
to the extent reasonably and technically practicable.
|
||||||
p.
|
</p>
|
||||||
|
<p>
|
||||||
If you are an end user and wish to update, delete, or receive any information we have about you, you may do so by
|
If you are an end user and wish to update, delete, or receive any information we have about you, you may do so by
|
||||||
contacting the organization of which you are a customer.
|
contacting the organization of which you are a customer.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Governing Law
|
<h4>Governing Law</h4>
|
||||||
p.
|
<p>
|
||||||
This Privacy Policy is governed by the laws of US without regard to its conflict of laws provision. You consent to
|
This Privacy Policy is governed by the laws of US without regard to its conflict of laws provision. You consent to
|
||||||
the exclusive jurisdiction of the courts in connection with any action or dispute arising between the parties under
|
the exclusive jurisdiction of the courts in connection with any action or dispute arising between the parties
|
||||||
or in connection with this Privacy Policy except for those individuals who may have rights to make claims under
|
under or in connection with this Privacy Policy except for those individuals who may have rights to make claims
|
||||||
Privacy Shield, or the Swiss-US framework.
|
under Privacy Shield, or the Swiss-US framework.
|
||||||
p.
|
</p>
|
||||||
The laws of US, excluding its conflicts of law rules, shall govern this Agreement and your use of the website. Your
|
<p>
|
||||||
use of the website may also be subject to other local, state, national, or international laws.
|
The laws of US, excluding its conflicts of law rules, shall govern this Agreement and your use of the website.
|
||||||
p.
|
Your use of the website may also be subject to other local, state, national, or international laws.
|
||||||
By using {{name}} or contacting us directly, you signify your acceptance of this Privacy Policy. If you do not agree
|
</p>
|
||||||
to this Privacy Policy, you should not engage with our website, or use our services. Continued use of the website,
|
<p>
|
||||||
direct engagement with us, or following the posting of changes to this Privacy Policy that do not significantly
|
By using {{name}} or contacting us directly, you signify your acceptance of this Privacy Policy. If you do not
|
||||||
affect the use or disclosure of your personal information will mean that you accept those changes.
|
agree to this Privacy Policy, you should not engage with our website, or use our services. Continued use of the
|
||||||
|
website, direct engagement with us, or following the posting of changes to this Privacy Policy that do not
|
||||||
|
significantly affect the use or disclosure of your personal information will mean that you accept those changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Your Consent
|
<h4>Your Consent</h4>
|
||||||
p.
|
<p>
|
||||||
We’ve updated our Privacy Policy to provide you with complete transparency into what is being set when you
|
We’ve updated our Privacy Policy to provide you with complete transparency into what is being set when you
|
||||||
visit our site and how it’s being used. By using our website, registering an account, or making a purchase,
|
visit our site and how it’s being used. By using our website, registering an account, or making a purchase,
|
||||||
you hereby consent to our Privacy Policy and agree to its terms.
|
you hereby consent to our Privacy Policy and agree to its terms.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Links to Other Websites
|
<h4>Links to Other Websites</h4>
|
||||||
p.
|
<p>
|
||||||
This Privacy Policy applies only to the Services. The Services may contain links to other websites not operated or
|
This Privacy Policy applies only to the Services. The Services may contain links to other websites not operated or
|
||||||
controlled by {{name}}. We are not responsible for the content, accuracy or opinions expressed in such websites, and
|
controlled by {{name}}. We are not responsible for the content, accuracy or opinions expressed in such websites,
|
||||||
such websites are not investigated, monitored or checked for accuracy or completeness by us. Please remember that
|
and such websites are not investigated, monitored or checked for accuracy or completeness by us. Please remember
|
||||||
when you use a link to go from the Services to another website, our Privacy Policy is no longer in effect. Your
|
that when you use a link to go from the Services to another website, our Privacy Policy is no longer in effect.
|
||||||
browsing and interaction on any other website, including those that have a link on our platform, is subject to that
|
Your browsing and interaction on any other website, including those that have a link on our platform, is subject
|
||||||
website’s own rules and policies. Such third parties may use their own cookies or other methods to collect
|
to that website’s own rules and policies. Such third parties may use their own cookies or other methods to collect
|
||||||
information about you.
|
information about you.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Cookies
|
<h4>Cookies</h4>
|
||||||
p {{name}} does not use Cookies.
|
<p>{{name}} does not use Cookies.</p>
|
||||||
|
|
||||||
h4 Kids’ Privacy
|
<h4>Kids’ Privacy</h4>
|
||||||
p.
|
<p>
|
||||||
We do not address anyone under the age of 13. We do not knowingly collect personally identifiable information from
|
We do not address anyone under the age of 13. We do not knowingly collect personally identifiable information from
|
||||||
anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with
|
anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with
|
||||||
Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age
|
Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the
|
||||||
of 13 without verification of parental consent, We take steps to remove that information from Our servers.
|
age of 13 without verification of parental consent, We take steps to remove that information from Our servers.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Changes To Our Privacy Policy
|
<h4>Changes To Our Privacy Policy</h4>
|
||||||
p.
|
<p>
|
||||||
We may change our Service and policies, and we may need to make changes to this Privacy Policy so that they
|
We may change our Service and policies, and we may need to make changes to this Privacy Policy so that they
|
||||||
accurately reflect our Service and policies. Unless otherwise required by law, we will notify you (for example,
|
accurately reflect our Service and policies. Unless otherwise required by law, we will notify you (for example,
|
||||||
through our Service) before we make changes to this Privacy Policy and give you an opportunity to review them before
|
through our Service) before we make changes to this Privacy Policy and give you an opportunity to review them
|
||||||
they go into effect. Then, if you continue to use the Service, you will be bound by the updated Privacy Policy. If
|
before they go into effect. Then, if you continue to use the Service, you will be bound by the updated Privacy
|
||||||
you do not want to agree to this or any updated Privacy Policy, you can delete your account.
|
Policy. If you do not want to agree to this or any updated Privacy Policy, you can delete your account.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Third-Party Services
|
<h4>Third-Party Services</h4>
|
||||||
p.
|
<p>
|
||||||
We may display, include or make available third-party content (including data, information, applications and other
|
We may display, include or make available third-party content (including data, information, applications and other
|
||||||
products services) or provide links to third-party websites or services (“Third-Party Services”).
|
products services) or provide links to third-party websites or services (“Third-Party Services”).
|
||||||
p.
|
</p>
|
||||||
|
<p>
|
||||||
You acknowledge and agree that {{name}} shall not be responsible for any Third-Party Services, including their
|
You acknowledge and agree that {{name}} shall not be responsible for any Third-Party Services, including their
|
||||||
accuracy, completeness, timeliness, validity, copyright compliance, legality, decency, quality or any other aspect
|
accuracy, completeness, timeliness, validity, copyright compliance, legality, decency, quality or any other aspect
|
||||||
thereof. {{name}} does not assume and shall not have any liability or responsibility to you or any other person or
|
thereof. {{name}} does not assume and shall not have any liability or responsibility to you or any other person or
|
||||||
entity for any Third-Party Services.
|
entity for any Third-Party Services.
|
||||||
p.
|
</p>
|
||||||
|
<p>
|
||||||
Third-Party Services and links thereto are provided solely as a convenience to you and you access and use them
|
Third-Party Services and links thereto are provided solely as a convenience to you and you access and use them
|
||||||
entirely at your own risk and subject to such third parties’ terms and conditions.
|
entirely at your own risk and subject to such third parties’ terms and conditions.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Tracking Technologies
|
<h4>Tracking Technologies</h4>
|
||||||
p.
|
<p>
|
||||||
{{name}} does not use any tracking technologies. When an authorization code is received from Mastodon, that token is
|
{{name}} does not use any tracking technologies. When a user signs in, a token is issued; that token is stored in
|
||||||
stored in the browser’s memory, and the Service uses tokens on each request for data. If the page is refreshed
|
the browser’s memory, and the Service uses it on each request for data. If the page is refreshed or the
|
||||||
or the browser window/tab is closed, this token disappears, and a new one must be generated before the application
|
browser window/tab is closed, this token disappears, and a new one must be generated before the application can be
|
||||||
can be used again.
|
used again.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 Information about General Data Protection Regulation (GDPR)
|
<h4>Information about General Data Protection Regulation (GDPR)</h4>
|
||||||
p.
|
<p>
|
||||||
We may be collecting and using information from you if you are from the European Economic Area (EEA), and in this
|
We may be collecting and using information from you if you are from the European Economic Area (EEA), and in this
|
||||||
section of our Privacy Policy we are going to explain exactly how and why is this data collected, and how we
|
section of our Privacy Policy we are going to explain exactly how and why is this data collected, and how we
|
||||||
maintain this data under protection from being replicated or used in the wrong way.
|
maintain this data under protection from being replicated or used in the wrong way.
|
||||||
|
</p>
|
||||||
|
|
||||||
h5 What is GDPR?
|
<h5>What is GDPR?</h5>
|
||||||
p.
|
<p>
|
||||||
GDPR is an EU-wide privacy and data protection law that regulates how EU residents’ data is protected by
|
GDPR is an EU-wide privacy and data protection law that regulates how EU residents’ data is protected by
|
||||||
companies and enhances the control the EU residents have, over their personal data.
|
companies and enhances the control the EU residents have, over their personal data.
|
||||||
p.
|
</p>
|
||||||
|
<p>
|
||||||
The GDPR is relevant to any globally operating company and not just the EU-based businesses and EU residents. Our
|
The GDPR is relevant to any globally operating company and not just the EU-based businesses and EU residents. Our
|
||||||
customers’ data is important irrespective of where they are located, which is why we have implemented GDPR controls
|
customers’ data is important irrespective of where they are located, which is why we have implemented GDPR
|
||||||
as our baseline standard for all our operations worldwide.
|
controls as our baseline standard for all our operations worldwide.
|
||||||
|
</p>
|
||||||
|
|
||||||
h5 What is personal data?
|
<h5>What is personal data?</h5>
|
||||||
p.
|
<p>
|
||||||
Any data that relates to an identifiable or identified individual. GDPR covers a broad spectrum of information that
|
Any data that relates to an identifiable or identified individual. GDPR covers a broad spectrum of information
|
||||||
could be used on its own, or in combination with other pieces of information, to identify a person. Personal data
|
that could be used on its own, or in combination with other pieces of information, to identify a person. Personal
|
||||||
extends beyond a person’s name or email address. Some examples include financial information, political opinions,
|
data extends beyond a person’s name or email address. Some examples include financial information, political
|
||||||
genetic data, biometric data, IP addresses, physical address, sexual orientation, and ethnicity.
|
opinions, genetic data, biometric data, IP addresses, physical address, sexual orientation, and ethnicity.
|
||||||
p The Data Protection Principles include requirements such as:
|
</p>
|
||||||
ul
|
<p>The Data Protection Principles include requirements such as:</p>
|
||||||
li.
|
<ul>
|
||||||
|
<li>
|
||||||
Personal data collected must be processed in a fair, legal, and transparent way and should only be used in a way
|
Personal data collected must be processed in a fair, legal, and transparent way and should only be used in a way
|
||||||
that a person would reasonably expect.
|
that a person would reasonably expect.
|
||||||
li.
|
</li>
|
||||||
|
<li>
|
||||||
Personal data should only be collected to fulfil a specific purpose and it should only be used for that purpose.
|
Personal data should only be collected to fulfil a specific purpose and it should only be used for that purpose.
|
||||||
Organizations must specify why they need the personal data when they collect it.
|
Organizations must specify why they need the personal data when they collect it.
|
||||||
li Personal data should be held no longer than necessary to fulfil its purpose.
|
</li>
|
||||||
li.
|
<li>Personal data should be held no longer than necessary to fulfil its purpose.</li>
|
||||||
People covered by the GDPR have the right to access their own personal data. They can also request a copy of their
|
<li>
|
||||||
data, and that their data be updated, deleted, restricted, or moved to another organization.
|
People covered by the GDPR have the right to access their own personal data. They can also request a copy of
|
||||||
|
their data, and that their data be updated, deleted, restricted, or moved to another organization.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
h5 Why is GDPR important?
|
<h5>Why is GDPR important?</h5>
|
||||||
p.
|
<p>
|
||||||
GDPR adds some new requirements regarding how companies should protect individuals’ personal data that they
|
GDPR adds some new requirements regarding how companies should protect individuals’ personal data that they
|
||||||
collect and process. It also raises the stakes for compliance by increasing enforcement and imposing greater fines
|
collect and process. It also raises the stakes for compliance by increasing enforcement and imposing greater fines
|
||||||
for breach. Beyond these facts, it’s simply the right thing to do. At {{name}} we strongly believe that your
|
for breach. Beyond these facts, it’s simply the right thing to do. At {{name}} we strongly believe that your
|
||||||
data privacy is very important and we already have solid security and privacy practices in place that go beyond the
|
data privacy is very important and we already have solid security and privacy practices in place that go beyond
|
||||||
requirements of this regulation.
|
the requirements of this regulation.
|
||||||
|
</p>
|
||||||
|
|
||||||
h5 Individual Data Subject’s Rights - Data Access, Portability, and Deletion
|
<h5>Individual Data Subject’s Rights - Data Access, Portability, and Deletion</h5>
|
||||||
p.
|
<p>
|
||||||
We are committed to helping our customers meet the data subject rights requirements of GDPR. {{name}} processes or
|
We are committed to helping our customers meet the data subject rights requirements of GDPR. {{name}} processes or
|
||||||
stores all personal data in fully vetted, DPA compliant vendors. We do store all conversation and personal data for
|
stores all personal data in fully vetted, DPA compliant vendors. We do store all conversation and personal data
|
||||||
up to 6 years unless your account is deleted. In which case, we dispose of all data in accordance with our Terms of
|
for up to 6 years unless your account is deleted. In which case, we dispose of all data in accordance with our
|
||||||
Service and Privacy Policy, but we will not hold it longer than 60 days.
|
Terms of Service and Privacy Policy, but we will not hold it longer than 60 days.
|
||||||
p.
|
</p>
|
||||||
|
<p>
|
||||||
We are aware that if you are working with EU customers, you need to be able to provide them with the ability to
|
We are aware that if you are working with EU customers, you need to be able to provide them with the ability to
|
||||||
access, update, retrieve and remove personal data. We got you! We’ve been set up as self service from the
|
access, update, retrieve and remove personal data. We got you! We’ve been set up as self service from the
|
||||||
start and have always given you access to your data. Our customer support team is here for you to answer any
|
start and have always given you access to your data. Our customer support team is here for you to answer any
|
||||||
questions you might have about working with the API.
|
questions you might have about working with the API.
|
||||||
|
</p>
|
||||||
|
|
||||||
h4 California Residents
|
<h4>California Residents</h4>
|
||||||
p.
|
<p>
|
||||||
The California Consumer Privacy Act (CCPA) requires us to disclose categories of Personal Information we collect and
|
The California Consumer Privacy Act (CCPA) requires us to disclose categories of Personal Information we collect
|
||||||
how we use it, the categories of sources from whom we collect Personal Information, and the third parties with whom
|
and how we use it, the categories of sources from whom we collect Personal Information, and the third parties with
|
||||||
we share it, which we have explained above.
|
whom we share it, which we have explained above.
|
||||||
p.
|
</p>
|
||||||
We are also required to communicate information about rights California residents have under California law. You may
|
<p>
|
||||||
exercise the following rights:
|
We are also required to communicate information about rights California residents have under California law. You
|
||||||
ul
|
may exercise the following rights:
|
||||||
li.
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
Right to Know and Access. You may submit a verifiable request for information regarding the: (1) categories of
|
Right to Know and Access. You may submit a verifiable request for information regarding the: (1) categories of
|
||||||
Personal Information we collect, use, or share; (2) purposes for which categories of Personal Information are
|
Personal Information we collect, use, or share; (2) purposes for which categories of Personal Information are
|
||||||
collected or used by us; (3) categories of sources from which we collect Personal Information; and (4) specific
|
collected or used by us; (3) categories of sources from which we collect Personal Information; and (4) specific
|
||||||
pieces of Personal Information we have collected about you.
|
pieces of Personal Information we have collected about you.
|
||||||
li Right to Equal Service. We will not discriminate against you if you exercise your privacy rights.
|
</li>
|
||||||
li.
|
<li>Right to Equal Service. We will not discriminate against you if you exercise your privacy rights.</li>
|
||||||
Right to Delete. You may submit a verifiable request to close your account and we will delete Personal Information
|
<li>
|
||||||
about you that we have collected.
|
Right to Delete. You may submit a verifiable request to close your account and we will delete Personal
|
||||||
li Request that a business that sells a consumer’s personal data, not sell the consumer’s personal data.
|
Information about you that we have collected.
|
||||||
p.
|
</li>
|
||||||
|
<li>
|
||||||
|
Request that a business that sells a consumer’s personal data, not sell the consumer’s personal
|
||||||
|
data.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
If you make a request, we have one month to respond to you. If you would like to exercise any of these rights,
|
If you make a request, we have one month to respond to you. If you would like to exercise any of these rights,
|
||||||
please contact us.
|
please contact us.
|
||||||
p We do not sell the Personal Information of our users.
|
</p>
|
||||||
p For more information about these rights, please contact us.
|
<p>We do not sell the Personal Information of our users.</p>
|
||||||
|
<p>For more information about these rights, please contact us.</p>
|
||||||
|
|
||||||
h5 California Online Privacy Protection Act (CalOPPA)
|
<h5>California Online Privacy Protection Act (CalOPPA)</h5>
|
||||||
p.
|
<p>
|
||||||
CalOPPA requires us to disclose categories of Personal Information we collect and how we use it, the categories of
|
CalOPPA requires us to disclose categories of Personal Information we collect and how we use it, the categories of
|
||||||
sources from whom we collect Personal Information, and the third parties with whom we share it, which we have
|
sources from whom we collect Personal Information, and the third parties with whom we share it, which we have
|
||||||
explained above.
|
explained above.
|
||||||
p CalOPPA users have the following rights:
|
</p>
|
||||||
ul
|
<p>CalOPPA users have the following rights:</p>
|
||||||
li.
|
<ul>
|
||||||
|
<li>
|
||||||
Right to Know and Access. You may submit a verifiable request for information regarding the: (1) categories of
|
Right to Know and Access. You may submit a verifiable request for information regarding the: (1) categories of
|
||||||
Personal Information we collect, use, or share; (2) purposes for which categories of Personal Information are
|
Personal Information we collect, use, or share; (2) purposes for which categories of Personal Information are
|
||||||
collected or used by us; (3) categories of sources from which we collect Personal Information; and (4) specific
|
collected or used by us; (3) categories of sources from which we collect Personal Information; and (4) specific
|
||||||
pieces of Personal Information we have collected about you.
|
pieces of Personal Information we have collected about you.
|
||||||
li Right to Equal Service. We will not discriminate against you if you exercise your privacy rights.
|
</li>
|
||||||
li.
|
<li>Right to Equal Service. We will not discriminate against you if you exercise your privacy rights.</li>
|
||||||
Right to Delete. You may submit a verifiable request to close your account and we will delete Personal Information
|
<li>
|
||||||
about you that we have collected.
|
Right to Delete. You may submit a verifiable request to close your account and we will delete Personal
|
||||||
li.
|
Information about you that we have collected.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
Right to request that a business that sells a consumer’s personal data, not sell the consumer’s
|
Right to request that a business that sells a consumer’s personal data, not sell the consumer’s
|
||||||
personal data.
|
personal data.
|
||||||
p.
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
If you make a request, we have one month to respond to you. If you would like to exercise any of these rights,
|
If you make a request, we have one month to respond to you. If you would like to exercise any of these rights,
|
||||||
please contact us.
|
please contact us.
|
||||||
p We do not sell the Personal Information of our users.
|
</p>
|
||||||
p For more information about these rights, please contact us.
|
<p>We do not sell the Personal Information of our users.</p>
|
||||||
|
<p>For more information about these rights, please contact us.</p>
|
||||||
|
|
||||||
h4 Contact Us
|
<h4>Contact Us</h4>
|
||||||
p Don’t hesitate to contact us if you have any questions.
|
<p>Don’t hesitate to contact us if you have any questions.</p>
|
||||||
ul: li Via this Link: #[router-link(to="/how-it-works") https://noagendacareers.com/how-it-works]
|
<ul>
|
||||||
|
<li>Via this Link: <router-link to="/how-it-works">https://noagendacareers.com/how-it-works</router-link></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
hr
|
<hr>
|
||||||
|
|
||||||
p: em.
|
<p class="fst-italic">
|
||||||
Change on September 6#[sup th], 2021 – replaced “No Agenda Social” with generic terms for any
|
Changes on August 30<sup>th</sup>, 2022 –
|
||||||
authorized Mastodon instance.
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li class="fst-italic">Removed references to Mastodon</li>
|
||||||
|
<li class="fst-italic">Added references to job listings</li>
|
||||||
|
<li class="fst-italic">Changed information regarding e-mail addresses</li>
|
||||||
|
</ul>
|
||||||
|
<p class="fst-italic">
|
||||||
|
Change on September 6<sup>th</sup>, 2021 – replaced “No Agenda Social” with generic terms for
|
||||||
|
any authorized Mastodon instance.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
@ -1,62 +1,44 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3 Terms of Service
|
<h3>Terms of Service</h3>
|
||||||
p: em (as of September 6#[sup th], 2021)
|
<p class="fst-italic">(as of August 30<sup>th</sup>, 2022)</p>
|
||||||
|
<h4>Acceptance of Terms</h4>
|
||||||
h4 Acceptance of Terms
|
<p>
|
||||||
p.
|
By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you are
|
||||||
By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you are responsible
|
responsible to ensure that your use of this site complies with all applicable laws. Your continued use of this
|
||||||
to ensure that your use of this site complies with all applicable laws. Your continued use of this site implies your
|
site implies your acceptance of these terms.
|
||||||
acceptance of these terms.
|
</p>
|
||||||
|
<h4>Description of Service and Registration</h4>
|
||||||
h4 Description of Service and Registration
|
<p>
|
||||||
p
|
Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles and job listings,
|
||||||
| Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles, restricting access
|
restricting access to the details of these to other users of this site, unless the individual specifies that this
|
||||||
| to the details of these profiles to other users of No Agenda-afilliated Mastodon sites (currently
|
information should be visible publicly. See our <router-link to="/privacy-policy">privacy policy</router-link> for
|
||||||
= " "
|
details on the personal (user) information we maintain.
|
||||||
template(v-for="(it, idx) in instances" :key="idx")
|
</p>
|
||||||
a(:href="it.url" target="_blank") {{it.name}}
|
<h4>Liability</h4>
|
||||||
template(v-if="idx + 2 < instances.length")= ", "
|
<p>
|
||||||
template(v-else-if="idx + 1 < instances.length")= ", and "
|
|
||||||
| ). Registration is accomplished by allowing Jobs, Jobs, Jobs to read one’s Mastodon profile. See our
|
|
||||||
= " "
|
|
||||||
router-link(to="/privacy-policy") privacy policy
|
|
||||||
= " "
|
|
||||||
| for details on the personal (user) information we maintain.
|
|
||||||
|
|
||||||
h4 Liability
|
|
||||||
p.
|
|
||||||
This service is provided “as is”, and no warranty (express or implied) exists. The service and its
|
This service is provided “as is”, and no warranty (express or implied) exists. The service and its
|
||||||
developers may not be held liable for any damages that may arise through the use of this service.
|
developers may not be held liable for any damages that may arise through the use of this service.
|
||||||
|
</p>
|
||||||
h4 Updates to Terms
|
<h4>Updates to Terms</h4>
|
||||||
p.
|
<p>
|
||||||
These terms and conditions may be updated at any time. When these terms are updated, users will be notified via a
|
These terms and conditions may be updated at any time. When these terms are updated, users will be notified via a
|
||||||
notice on the dashboard page. Additionally, the date at the top of this page will be updated, and any substantive
|
notice on the dashboard page. Additionally, the date at the top of this page will be updated, and any substantive
|
||||||
updates will also be accompanied by a summary of those changes.
|
updates will also be accompanied by a summary of those changes.
|
||||||
|
</p>
|
||||||
hr
|
<hr>
|
||||||
|
<p>
|
||||||
p.
|
You may also wish to review our <router-link to="/privacy-policy">privacy policy</router-link> to learn how we
|
||||||
You may also wish to review our #[router-link(to="/privacy-policy") privacy policy] to learn how we handle your
|
handle your data.
|
||||||
data.
|
</p>
|
||||||
|
<hr>
|
||||||
hr
|
<p class="fst-italic">
|
||||||
|
Change on August 30<sup>th</sup>, 2022 – added references to job listings, removed references to Mastodon
|
||||||
p: em.
|
instances.
|
||||||
Change on September 6#[sup th], 2021 – replaced “No Agenda Social” with a list of all No
|
</p>
|
||||||
|
<p class="fst-italic">
|
||||||
|
Change on September 6<sup>th</sup>, 2021 – replaced “No Agenda Social” with a list of all No
|
||||||
Agenda-affiliated Mastodon instances.
|
Agenda-affiliated Mastodon instances.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted } from "vue"
|
|
||||||
import { useStore, Actions } from "@/store"
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
/** All instances authorized to view Jobs, Jobs, Jobs */
|
|
||||||
const instances = computed(() => store.state.instances)
|
|
||||||
|
|
||||||
onMounted(async () => { await store.dispatch(Actions.EnsureInstances) })
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
<template lang="pug">
|
|
||||||
article
|
|
||||||
p
|
|
||||||
p(v-html="message")
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted } from "vue"
|
|
||||||
import { useRoute, useRouter } from "vue-router"
|
|
||||||
import { useStore, Actions, Mutations } from "@/store"
|
|
||||||
import { AFTER_LOG_ON_URL } from "@/router"
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
/** The abbreviation of the instance from which we received the code */
|
|
||||||
const abbr = route.params.abbr as string
|
|
||||||
|
|
||||||
/** Set the message for this component */
|
|
||||||
const setMessage = (msg : string) => store.commit(Mutations.SetLogOnState, msg)
|
|
||||||
|
|
||||||
/** Pass the code to the API and exchange it for a user and a JWT */
|
|
||||||
const logOn = async () => {
|
|
||||||
await store.dispatch(Actions.EnsureInstances)
|
|
||||||
const instance = store.state.instances.find(it => it.abbr === abbr)
|
|
||||||
if (typeof instance === "undefined") {
|
|
||||||
setMessage(`Mastodon instance ${abbr} not found`)
|
|
||||||
} else {
|
|
||||||
setMessage(`<em>Welcome back! Verifying your ${instance.name} account…</em>`)
|
|
||||||
const code = route.query.code
|
|
||||||
if (code) {
|
|
||||||
await store.dispatch(Actions.LogOn, { abbr, code })
|
|
||||||
if (store.state.user !== undefined) {
|
|
||||||
const afterLogOnUrl = window.localStorage.getItem(AFTER_LOG_ON_URL)
|
|
||||||
if (afterLogOnUrl) {
|
|
||||||
window.localStorage.removeItem(AFTER_LOG_ON_URL)
|
|
||||||
router.push(afterLogOnUrl)
|
|
||||||
} else {
|
|
||||||
router.push("/citizen/dashboard")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setMessage(`Did not receive a token from ${instance.name} (perhaps you clicked “Cancel”?)`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(logOn)
|
|
||||||
|
|
||||||
/** Accessor for the log on state */
|
|
||||||
const message = computed(() => store.state.logOnState)
|
|
||||||
</script>
|
|
|
@ -1,44 +1,72 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article.container
|
<article class="container">
|
||||||
h3.pb-4 Welcome, {{user.name}}
|
<h3 class="pb-4">ITM, {{user.name}}!</h3>
|
||||||
load-data(:load="retrieveData"): .row.row-cols-1.row-cols-md-2
|
<load-data :load="retrieveData">
|
||||||
.col: .card.h-100
|
<div class="row row-cols-1 row-cols-md-2">
|
||||||
h5.card-header Your Profile
|
<div class="col">
|
||||||
.card-body
|
<div class="card h-100">
|
||||||
h6.card-subtitle.mb-3.text-muted.fst-italic(v-if="profile").
|
<h5 class="card-header">Your Profile</h5>
|
||||||
Last updated #[full-date-time(:date="profile.lastUpdatedOn")]
|
<div class="card-body">
|
||||||
p.card-text(v-if="profile")
|
<h6 v-if="profile" class="card-subtitle mb-3 text-muted fst-italic">
|
||||||
| Your profile currently lists {{profile.skills.length}}
|
Last updated <full-date-time :date="profile.lastUpdatedOn" />
|
||||||
| skill#[template(v-if="profile.skills.length !== 1") s].
|
</h6>
|
||||||
span(v-if="profile.seekingEmployment")
|
<p v-if="profile" class="card-text">
|
||||||
br
|
Your profile currently lists {{profile.skills.length}}
|
||||||
br
|
skill<template v-if="profile.skills.length !== 1">s</template>.
|
||||||
| Your profile indicates that you are seeking employment. Once you find it,
|
<span v-if="profile.seekingEmployment">
|
||||||
router-link(to="/success-story/add") tell your fellow citizens about it!
|
<br><br>
|
||||||
p.card-text(v-else).
|
Your profile indicates that you are seeking employment. Once you find it,
|
||||||
You do not have an employment profile established; click below (or “Edit Profile” in the menu) to
|
<router-link to="/success-story/add">tell your fellow citizens about it!</router-link>
|
||||||
get started!
|
</span>
|
||||||
.card-footer
|
</p>
|
||||||
template(v-if="profile")
|
<p class="card-text" v-else>
|
||||||
router-link.btn.btn-outline-secondary(:to="`/profile/${user.citizenId}/view`") View Profile
|
You do not have an employment profile established; click below (or “Edit Profile” in the
|
||||||
|
|
menu) to get started!
|
||||||
router-link.btn.btn-outline-secondary(to="/citizen/profile") Edit Profile
|
</p>
|
||||||
router-link.btn.btn-primary(v-else to="/citizen/profile") Create Profile
|
</div>
|
||||||
.col: .card.h-100
|
<div class="card-footer">
|
||||||
h5.card-header Other Citizens
|
<template v-if="profile">
|
||||||
.card-body
|
<router-link class="btn btn-outline-secondary"
|
||||||
h6.card-subtitle.mb-3.text-muted.fst-italic
|
:to="`/profile/${user.citizenId}/view`">View Profile</router-link>
|
||||||
template(v-if="profileCount === 0") No
|
|
||||||
template(v-else) {{profileCount}} Total
|
<router-link class="btn btn-outline-secondary" to="/profile/edit">Edit Profile</router-link>
|
||||||
| Employment Profile#[template(v-if="profileCount !== 1") s]
|
</template>
|
||||||
p.card-text(v-if="profileCount === 1 && profile") It looks like, for now, it’s just you…
|
<router-link class="btn btn-primary" v-else to="/profile/edit">Create Profile</router-link>
|
||||||
p.card-text(v-else-if="profileCount > 0") Take a look around and see if you can help them find work!
|
</div>
|
||||||
p.card-text(v-else) You can click below, but you will not find anything…
|
</div>
|
||||||
.card-footer: router-link.btn.btn-outline-secondary(to="/profile/search") Search Profiles
|
</div>
|
||||||
p
|
<div class="col">
|
||||||
p.
|
<div class="card h-100">
|
||||||
|
<h5 class="card-header">Other Citizens</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-subtitle mb-3 text-muted fst-italic">
|
||||||
|
<template v-if="profileCount === 0">No </template>
|
||||||
|
<template v-else>{{profileCount}} Total </template>
|
||||||
|
Employment Profile<template v-if="profileCount !== 1">s</template>
|
||||||
|
</h6>
|
||||||
|
<p v-if="profileCount === 1 && profile" class="card-text">
|
||||||
|
It looks like, for now, it’s just you…
|
||||||
|
</p>
|
||||||
|
<p v-else-if="profileCount > 0" class="card-text">
|
||||||
|
Take a look around and see if you can help them find work!
|
||||||
|
</p>
|
||||||
|
<p v-else class="card-text">
|
||||||
|
You can click below, but you will not find anything…
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<router-link class="btn btn-outline-secondary" to="/profile/search">Search Profiles</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</load-data>
|
||||||
|
<p> </p>
|
||||||
|
<p>
|
||||||
To see how this application works, check out “How It Works” in the sidebar (last updated August
|
To see how this application works, check out “How It Works” in the sidebar (last updated August
|
||||||
29#[sup th], 2021).
|
29<sup>th</sup>, 2021).
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
@ -1,184 +0,0 @@
|
||||||
<template lang="pug">
|
|
||||||
article
|
|
||||||
h3.pb-3 My Employment Profile
|
|
||||||
load-data(:load="retrieveData"): form.row.g-3
|
|
||||||
.col-12.col-sm-10.col-md-8.col-lg-6
|
|
||||||
.form-floating
|
|
||||||
input.form-control(type="text" id="realName" v-model="v$.realName.$model" maxlength="255"
|
|
||||||
placeholder="Leave blank to use your Mastodon display name")
|
|
||||||
label(for="realName") Real Name
|
|
||||||
.form-text Leave blank to use your Mastodon display name
|
|
||||||
.col-12
|
|
||||||
.form-check
|
|
||||||
input.form-check-input(type="checkbox" id="isSeeking" v-model="v$.isSeekingEmployment.$model")
|
|
||||||
label.form-check-label(for="isSeeking") I am currently seeking employment
|
|
||||||
p(v-if="profile.isSeekingEmployment"): em.
|
|
||||||
If you have found employment, consider
|
|
||||||
#[router-link(to="/success-story/new/edit") telling your fellow citizens about it!]
|
|
||||||
.col-12.col-sm-6.col-md-4
|
|
||||||
continent-list(v-model="v$.continentId.$model" :isInvalid="v$.continentId.$error"
|
|
||||||
@touch="v$.continentId.$touch() || true")
|
|
||||||
.col-12.col-sm-6.col-md-8
|
|
||||||
.form-floating
|
|
||||||
input.form-control(type="text" id="region" :class="{ 'is-invalid': v$.region.$error }"
|
|
||||||
v-model="v$.region.$model" maxlength="255"
|
|
||||||
placeholder="Country, state, geographic area, etc.")
|
|
||||||
#regionFeedback.invalid-feedback Please enter a region
|
|
||||||
label.jjj-required(for="region") Region
|
|
||||||
.form-text Country, state, geographic area, etc.
|
|
||||||
markdown-editor(id="bio" label="Professional Biography" v-model:text="v$.biography.$model"
|
|
||||||
:isInvalid="v$.biography.$error")
|
|
||||||
.col-12.col-offset-md-2.col-md-4
|
|
||||||
.form-check
|
|
||||||
input.form-check-input(type="checkbox" id="isRemote" v-model="v$.remoteWork.$model")
|
|
||||||
label.form-check-label(for="isRemote") I am looking for remote work
|
|
||||||
.col-12.col-md-4
|
|
||||||
.form-check
|
|
||||||
input.form-check-input(type="checkbox" id="isFullTime" v-model="v$.fullTime.$model")
|
|
||||||
label.form-check-label(for="isFullTime") I am looking for full-time work
|
|
||||||
.col-12
|
|
||||||
hr
|
|
||||||
h4.pb-2 Skills #[button.btn.btn-sm.btn-outline-primary.rounded-pill(@click.prevent="addSkill") Add a Skill]
|
|
||||||
profile-skill-edit(v-for="(skill, idx) in profile.skills" :key="skill.id" v-model="profile.skills[idx]"
|
|
||||||
@remove="removeSkill(skill.id)" @input="v$.skills.$touch")
|
|
||||||
.col-12
|
|
||||||
hr
|
|
||||||
h4 Experience
|
|
||||||
p.
|
|
||||||
This application does not have a place to individually list your chronological job history; however, you can use
|
|
||||||
this area to list prior jobs, their dates, and anything else you want to include that’s not already a part
|
|
||||||
of your Professional Biography above.
|
|
||||||
markdown-editor(id="experience" label="Experience" v-model:text="v$.experience.$model")
|
|
||||||
.col-12: .form-check
|
|
||||||
input.form-check-input(type="checkbox" id="isPublic" v-model="v$.isPublic.$model")
|
|
||||||
label.form-check-label(for="isPublic") Allow my profile to be searched publicly (outside NA Social)
|
|
||||||
.col-12
|
|
||||||
p.text-danger(v-if="v$.$error") Please correct the errors above
|
|
||||||
button.btn.btn-primary(@click.prevent="saveProfile") #[icon(:icon="mdiContentSaveOutline")] Save
|
|
||||||
template(v-if="!isNew")
|
|
||||||
|
|
|
||||||
router-link.btn.btn-outline-secondary(:to="`/profile/${user.citizenId}/view`").
|
|
||||||
#[icon(color="#6c757d" :icon="mdiFileAccountOutline")] View Your User Profile
|
|
||||||
hr
|
|
||||||
p.text-muted.fst-italic.
|
|
||||||
(If you want to delete your profile, or your entire account,
|
|
||||||
#[router-link(to="/so-long/options") see your deletion options here].)
|
|
||||||
maybe-save(:saveAction="saveProfile" :validator="v$")
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, reactive } from "vue"
|
|
||||||
import { mdiContentSaveOutline, mdiFileAccountOutline } from "@mdi/js"
|
|
||||||
import useVuelidate from "@vuelidate/core"
|
|
||||||
import { required } from "@vuelidate/validators"
|
|
||||||
|
|
||||||
import api, { Citizen, LogOnSuccess, Profile, ProfileForm } from "@/api"
|
|
||||||
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
|
|
||||||
import { useStore } from "@/store"
|
|
||||||
|
|
||||||
import ContinentList from "@/components/ContinentList.vue"
|
|
||||||
import LoadData from "@/components/LoadData.vue"
|
|
||||||
import MarkdownEditor from "@/components/MarkdownEditor.vue"
|
|
||||||
import MaybeSave from "@/components/MaybeSave.vue"
|
|
||||||
import ProfileSkillEdit from "@/components/profile/SkillEdit.vue"
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
/** The currently logged-on user */
|
|
||||||
const user = store.state.user as LogOnSuccess
|
|
||||||
|
|
||||||
/** Whether this is a new profile */
|
|
||||||
const isNew = ref(false)
|
|
||||||
|
|
||||||
/** The starting values for a new employment profile */
|
|
||||||
const newProfile : Profile = {
|
|
||||||
id: user.citizenId,
|
|
||||||
seekingEmployment: false,
|
|
||||||
isPublic: false,
|
|
||||||
continentId: "",
|
|
||||||
region: "",
|
|
||||||
remoteWork: false,
|
|
||||||
fullTime: false,
|
|
||||||
biography: "",
|
|
||||||
lastUpdatedOn: "",
|
|
||||||
experience: undefined,
|
|
||||||
skills: []
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The user's current profile (plus a few items, adapted for editing) */
|
|
||||||
const profile = reactive(new ProfileForm())
|
|
||||||
|
|
||||||
/** The validation rules for the form */
|
|
||||||
const rules = computed(() => ({
|
|
||||||
realName: { },
|
|
||||||
isSeekingEmployment: { },
|
|
||||||
isPublic: { },
|
|
||||||
continentId: { required },
|
|
||||||
region: { required },
|
|
||||||
remoteWork: { },
|
|
||||||
fullTime: { },
|
|
||||||
biography: { required },
|
|
||||||
experience: { },
|
|
||||||
skills: { }
|
|
||||||
}))
|
|
||||||
|
|
||||||
/** Initialize form validation */
|
|
||||||
const v$ = useVuelidate(rules, profile, { $lazy: true })
|
|
||||||
|
|
||||||
/** Retrieve the user's profile and their real name */
|
|
||||||
const retrieveData = async (errors : string[]) => {
|
|
||||||
const profileResult = await api.profile.retreive(undefined, user)
|
|
||||||
if (typeof profileResult === "string") {
|
|
||||||
errors.push(profileResult)
|
|
||||||
} else if (typeof profileResult === "undefined") {
|
|
||||||
isNew.value = true
|
|
||||||
}
|
|
||||||
const nameResult = await api.citizen.retrieve(user.citizenId, user)
|
|
||||||
if (typeof nameResult === "string") {
|
|
||||||
errors.push(nameResult)
|
|
||||||
}
|
|
||||||
if (errors.length > 0) return
|
|
||||||
// Update the empty form with appropriate values
|
|
||||||
const p = isNew.value ? newProfile : profileResult as Profile
|
|
||||||
profile.isSeekingEmployment = p.seekingEmployment
|
|
||||||
profile.isPublic = p.isPublic
|
|
||||||
profile.continentId = p.continentId
|
|
||||||
profile.region = p.region
|
|
||||||
profile.remoteWork = p.remoteWork
|
|
||||||
profile.fullTime = p.fullTime
|
|
||||||
profile.biography = p.biography
|
|
||||||
profile.experience = p.experience
|
|
||||||
profile.skills = p.skills
|
|
||||||
profile.realName = typeof nameResult !== "undefined" ? (nameResult as Citizen).realName ?? "" : ""
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The ID for new skills */
|
|
||||||
let newSkillId = 0
|
|
||||||
|
|
||||||
/** Add a skill to the profile */
|
|
||||||
const addSkill = () => {
|
|
||||||
profile.skills.push({ id: `new${newSkillId++}`, description: "", notes: undefined })
|
|
||||||
v$.value.skills.$touch()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Remove the given skill from the profile */
|
|
||||||
const removeSkill = (skillId : string) => {
|
|
||||||
profile.skills = profile.skills.filter(s => s.id !== skillId)
|
|
||||||
v$.value.skills.$touch()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Save the current profile values */
|
|
||||||
const saveProfile = async () => {
|
|
||||||
v$.value.$touch()
|
|
||||||
if (v$.value.$error) return
|
|
||||||
// Remove any blank skills before submitting
|
|
||||||
profile.skills = profile.skills.filter(s => !(s.description.trim() === "" && (s.notes ?? "").trim() === ""))
|
|
||||||
const saveResult = await api.profile.save(profile, user)
|
|
||||||
if (typeof saveResult === "string") {
|
|
||||||
toastError(saveResult, "saving profile")
|
|
||||||
} else {
|
|
||||||
toastSuccess("Profile Saved Successfuly")
|
|
||||||
v$.value.$reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,7 +1,8 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
p
|
<p> </p>
|
||||||
p.fst-italic Logging off…
|
<p class="fst-italic">Logging off…</p>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -13,9 +14,9 @@ import { useStore, Mutations } from "@/store"
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
store.commit(Mutations.ClearUser)
|
store.commit(Mutations.ClearUser)
|
||||||
toastSuccess("Log Off Successful | <strong>Have a Nice Day!</strong>")
|
toastSuccess("Log Off Successful | <strong>Have a Nice Day!</strong>")
|
||||||
router.push("/")
|
await router.push("/")
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,54 +1,92 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
p
|
<h3 class="pb-3">Log On</h3>
|
||||||
p.fst-italic(v-if="selected") Sending you over to {{selected.name}} to log on; see you back in just a second…
|
<p v-if="message !== ''" class="pb-3 text-center">
|
||||||
template(v-else)
|
<span class="text-danger">{{message}}</span><br>
|
||||||
p.text-center Please select your No Agenda-affiliated Mastodon instance
|
<template v-if="message.indexOf('ocked') > -1">
|
||||||
p.text-center(v-for="it in instances" :key="it.abbr")
|
If this is a new account, it must be confirmed before it can be used; otherwise, you need to
|
||||||
template(v-if="it.isEnabled")
|
<router-link to="/citizen/forgot-password">request an unlock code</router-link> before you may log on.
|
||||||
button.btn.btn-primary(@click.prevent="select(it.abbr)") {{it.name}}
|
</template>
|
||||||
template(v-else).
|
</p>
|
||||||
#[button.btn.btn-secondary(disabled="disabled") {{it.name}}]#[br]#[em {{it.reason}}]
|
<form class="row g-3 pb-3">
|
||||||
p: router-link(to="/citizen/register") Register
|
<div class="col-12 col-md-6">
|
||||||
|
<div class="form-floating">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="email" id="email" :class="{ 'form-control': true, 'is-invalid': v$.email.$error }"
|
||||||
|
v-model="v$.email.$model" placeholder="E-mail Address">
|
||||||
|
<div class="invalid-feedback">Please enter a valid e-mail address</div>
|
||||||
|
<label class="jjj-required" for="email">E-mail Address</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="password" id="password" :class="{ 'form-control': true, 'is-invalid': v$.password.$error }"
|
||||||
|
v-model="v$.password.$model" placeholder="Password">
|
||||||
|
<div class="invalid-feedback">Please enter a password</div>
|
||||||
|
<label class="jjj-required" for="password">Password</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<p class="text-danger" v-if="v$.$error">Please correct the errors above</p>
|
||||||
|
<button class="btn btn-primary" @click.prevent="logOn" :disabled="!logOnEnabled">
|
||||||
|
<icon :icon="mdiLogin" /> Log On
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p class="text-center">Need an account? <router-link to="/citizen/register">Register for one!</router-link></p>
|
||||||
|
<p class="text-center">
|
||||||
|
Forgot your password? <router-link to="/citizen/forgot-password">Request a reset.</router-link>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, Ref, ref } from "vue"
|
import { computed, reactive, ref } from "vue"
|
||||||
import { Instance } from "@/api"
|
import { useRouter } from "vue-router"
|
||||||
|
import { mdiLogin } from "@mdi/js"
|
||||||
|
import useVuelidate from "@vuelidate/core"
|
||||||
|
import { email, required } from "@vuelidate/validators"
|
||||||
|
|
||||||
|
import { LogOnForm } from "@/api"
|
||||||
|
import { toastSuccess } from "@/components/layout/AppToaster.vue"
|
||||||
|
import { AFTER_LOG_ON_URL } from "@/router"
|
||||||
import { useStore, Actions } from "@/store"
|
import { useStore, Actions } from "@/store"
|
||||||
|
|
||||||
import LoadData from "@/components/LoadData.vue"
|
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
/** The instances configured for Jobs, Jobs, Jobs */
|
/** The form to log on to Jobs, Jobs, Jobs */
|
||||||
const instances = computed(() => store.state.instances)
|
const logOnForm = reactive(new LogOnForm())
|
||||||
|
|
||||||
/** Whether authorization is in progress */
|
/** Whether the log on button is enabled */
|
||||||
const selected : Ref<Instance | undefined> = ref(undefined)
|
const logOnEnabled = ref(true)
|
||||||
|
|
||||||
/** The authorization URL to which the user should be directed */
|
/** The message returned from the log on attempt */
|
||||||
const authUrl = computed(() => {
|
const message = computed(() => store.state.logOnState)
|
||||||
if (selected.value) {
|
|
||||||
const client = `client_id=${selected.value.clientId}`
|
/** Validation rules for the log on form */
|
||||||
const scope = "scope=read:accounts"
|
const rules = computed(() => ({
|
||||||
const redirect = `redirect_uri=${document.location.origin}/citizen/${selected.value.abbr}/authorized`
|
email: { required, email },
|
||||||
const respType = "response_type=code"
|
password: { required }
|
||||||
return `${selected.value.url}/oauth/authorize?${client}&${scope}&${redirect}&${respType}`
|
}))
|
||||||
|
|
||||||
|
/** Form and validation */
|
||||||
|
const v$ = useVuelidate(rules, logOnForm, { $lazy: true })
|
||||||
|
|
||||||
|
/** Log the citizen on */
|
||||||
|
const logOn = async () => {
|
||||||
|
v$.value.$touch()
|
||||||
|
if (v$.value.$error) return
|
||||||
|
logOnEnabled.value = false
|
||||||
|
await store.dispatch(Actions.LogOn, { form: logOnForm })
|
||||||
|
logOnEnabled.value = true
|
||||||
|
if (store.state.user !== undefined) {
|
||||||
|
toastSuccess("Log On Successful")
|
||||||
|
v$.value.$reset()
|
||||||
|
const nextUrl = window.localStorage.getItem(AFTER_LOG_ON_URL) ?? "/citizen/dashboard"
|
||||||
|
window.localStorage.removeItem(AFTER_LOG_ON_URL)
|
||||||
|
await router.push(nextUrl)
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a given Mastodon instance
|
|
||||||
*
|
|
||||||
* @param abbr The abbreviation of the instance being selected
|
|
||||||
*/
|
|
||||||
const select = (abbr : string) => {
|
|
||||||
selected.value = instances.value.find(it => it.abbr === abbr)
|
|
||||||
document.location.assign(authUrl.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => { await store.dispatch(Actions.EnsureInstances) })
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,28 +1,40 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3.pb-3 Help Wanted
|
<h3 class="pb-3">Help Wanted</h3>
|
||||||
p(v-if="!searched").
|
<p v-if="!searched">
|
||||||
Enter relevant criteria to find results, or just click “Search” to see all current job listings.
|
Enter relevant criteria to find results, or just click “Search” to see all current job listings.
|
||||||
collapse-panel(headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse")
|
</p>
|
||||||
listing-search-form(v-model="criteria" @search="doSearch")
|
<collapse-panel headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse">
|
||||||
error-list(:errors="errors")
|
<listing-search-form v-model="criteria" @search="doSearch" />
|
||||||
p.pt-3(v-if="searching") Searching job listings…
|
</collapse-panel>
|
||||||
template(v-else)
|
<error-list :errors="errors">
|
||||||
table.table.table-sm.table-hover.pt-3(v-if="results.length > 0")
|
<p v-if="searching" class="pt-3">Searching job listings…</p>
|
||||||
thead: tr
|
<template v-else>
|
||||||
th(scope="col") Listing
|
<table v-if="results.length > 0" class="table table-sm table-hover pt-3">
|
||||||
th(scope="col") Title
|
<thead>
|
||||||
th(scope="col") Location
|
<tr>
|
||||||
th.text-center(scope="col") Remote?
|
<th scope="col">Listing</th>
|
||||||
th.text-center(scope="col") Needed By
|
<th scope="col">Title</th>
|
||||||
tbody: tr(v-for="it in results" :key="it.listing.id")
|
<th scope="col">Location</th>
|
||||||
td: router-link(:to="`/listing/${it.listing.id}/view`") View
|
<th class="text-center" scope="col">Remote?</th>
|
||||||
td {{it.listing.title}}
|
<th class="text-center" scope="col">Needed By</th>
|
||||||
td {{it.continent.name}} / {{it.listing.region}}
|
</tr>
|
||||||
td.text-center {{yesOrNo(it.listing.remoteWork)}}
|
</thead>
|
||||||
td.text-center(v-if="it.listing.neededBy") {{formatNeededBy(it.listing.neededBy)}}
|
<tbody>
|
||||||
td.text-center(v-else) N/A
|
<tr v-for="it in results" :key="it.listing.id">
|
||||||
p.pt-3(v-else-if="searched") No job listings found for the specified criteria
|
<td><router-link :to="`/listing/${it.listing.id}/view`">View</router-link></td>
|
||||||
|
<td>{{it.listing.title}}</td>
|
||||||
|
<td>{{it.continent.name}} / {{it.listing.region}}</td>
|
||||||
|
<td class="text-center">{{yesOrNo(it.listing.remoteWork)}}</td>
|
||||||
|
<td class="text-center" v-if="it.listing.neededBy">{{formatNeededBy(it.listing.neededBy)}}</td>
|
||||||
|
<td class="text-center" v-else>N/A</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p v-else-if="searched" class="pt-3">No job listings found for the specified criteria</p>
|
||||||
|
</template>
|
||||||
|
</error-list>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
@ -1,37 +1,58 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3.pb-3(v-if="isNew") Add a Job Listing
|
<h3 class="pb-3" v-if="isNew">Add a Job Listing</h3>
|
||||||
h3.pb-3(v-else) Edit Job Listing
|
<h3 class="pb-3" v-else>Edit Job Listing</h3>
|
||||||
load-data(:load="retrieveData"): form.row.g-3
|
<load-data :load="retrieveData">
|
||||||
.col-12.col-sm-10.col-md-8.col-lg-6
|
<form class="row g-3">
|
||||||
.form-floating
|
<div class="col-12 col-sm-10 col-md-8 col-lg-6">
|
||||||
input.form-control(type="text" id="title" :class="{ 'is-invalid': v$.title.$error }" maxlength="255"
|
<div class="form-floating">
|
||||||
v-model="v$.title.$model" placeholder="The title for the job listing")
|
<input type="text" id="title" :class="{ 'form-control': true, 'is-invalid': v$.title.$error }"
|
||||||
#titleFeedback.invalid-feedback Please enter a title for the job listing
|
maxlength="255" v-model="v$.title.$model" placeholder="The title for the job listing">
|
||||||
label.jjj-required(for="title") Title
|
<div class="invalid-feedback">Please enter a title for the job listing</div>
|
||||||
.form-text No need to put location here; it will always be show to seekers with continent and region
|
<label class="jjj-required" for="title">Title</label>
|
||||||
.col-12.col-sm-6.col-md-4
|
</div>
|
||||||
continent-list(v-model="v$.continentId.$model" :isInvalid="v$.continentId.$error"
|
<div class="form-text">
|
||||||
@touch="v$.continentId.$touch() || true")
|
No need to put location here; it will always be show to seekers with continent and region
|
||||||
.col-12.col-sm-6.col-md-8
|
</div>
|
||||||
.form-floating
|
</div>
|
||||||
input.form-control(type="text" id="region" :class="{ 'is-invalid': v$.region.$error }" maxlength="255"
|
<div class="col-12 col-sm-6 col-md-4">
|
||||||
v-model="v$.region.$model" placeholder="Country, state, geographic area, etc.")
|
<continent-list v-model="v$.continentId.$model" :isInvalid="v$.continentId.$error"
|
||||||
#regionFeedback.invalid-feedback Please enter a region
|
@touch="v$.continentId.$touch() || true" />
|
||||||
label.jjj-required(for="region") Region
|
</div>
|
||||||
.form-text Country, state, geographic area, etc.
|
<div class="col-12 col-sm-6 col-md-8">
|
||||||
.col-12: .form-check
|
<div class="form-floating">
|
||||||
input.form-check-input(type="checkbox" id="isRemote" v-model="v$.remoteWork.$model")
|
<input type="text" id="region" :class="{ 'form-control': true, 'is-invalid': v$.region.$error }"
|
||||||
label.form-check-label(for="isRemote") This opportunity is for remote work
|
maxlength="255" v-model="v$.region.$model" placeholder="Country, state, geographic area, etc.">
|
||||||
markdown-editor(id="description" label="Job Description" v-model:text="v$.text.$model" :isInvalid="v$.text.$error")
|
<div class="invalid-feedback">Please enter a region</div>
|
||||||
.col-12.col-md-4: .form-floating
|
<label class="jjj-required" for="region">Region</label>
|
||||||
input.form-control(type="date" id="neededBy" v-model="v$.neededBy.$model"
|
</div>
|
||||||
placeholder="Date by which this position needs to be filled")
|
<div class="form-text">Country, state, geographic area, etc.</div>
|
||||||
label(for="neededBy") Needed By
|
</div>
|
||||||
.col-12
|
<div class="col-12">
|
||||||
p.text-danger(v-if="v$.$error") Please correct the errors above
|
<div class="form-check">
|
||||||
button.btn.btn-primary(@click.prevent="saveListing(true)") #[icon(:icon="mdiContentSaveOutline")] Save
|
<input type="checkbox" id="isRemote" class="form-check-input" v-model="v$.remoteWork.$model">
|
||||||
maybe-save(:saveAction="doSave" :validator="v$")
|
<label class="form-check-label" for="isRemote">This opportunity is for remote work</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<markdown-editor id="description" label="Job Description" v-model:text="v$.text.$model"
|
||||||
|
:isInvalid="v$.text.$error" />
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="date" id="neededBy" class="form-control" v-model="v$.neededBy.$model"
|
||||||
|
placeholder="Date by which this position needs to be filled">
|
||||||
|
<label for="neededBy">Needed By</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<p v-if="v$.$error" class="text-danger">Please correct the errors above</p>
|
||||||
|
<button class="btn btn-primary" @click.prevent="saveListing(true)">
|
||||||
|
<icon :icon="mdiContentSaveOutline" /> Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</load-data>
|
||||||
|
<maybe-save :saveAction="doSave" :validator="v$" />
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -127,7 +148,7 @@ const saveListing = async (navigate : boolean) => {
|
||||||
} else {
|
} else {
|
||||||
toastSuccess(`Job Listing ${isNew.value ? "Add" : "Updat"}ed Successfully`)
|
toastSuccess(`Job Listing ${isNew.value ? "Add" : "Updat"}ed Successfully`)
|
||||||
v$.value.$reset()
|
v$.value.$reset()
|
||||||
if (navigate) router.push("/listings/mine")
|
if (navigate) await router.push("/listings/mine")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,36 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
load-data(:load="retrieveListing")
|
<load-data :load="retrieveListing">
|
||||||
h3.pb-3 Expire Job Listing ({{listing.title}})
|
<h3 class="pb-3">Expire Job Listing ({{listing.title}})</h3>
|
||||||
p: em.
|
<p class="fst-italic">
|
||||||
Expiring this listing will remove it from search results. You will be able to see it via your “My Job
|
Expiring this listing will remove it from search results. You will be able to see it via your “My Job
|
||||||
Listings” page, but you will not be able to “un-expire” it.
|
Listings” page, but you will not be able to “un-expire” it.
|
||||||
form.row.g-3
|
</p>
|
||||||
.col-12: .form-check
|
<form class="row g-3">
|
||||||
input.form-check-input(type="checkbox" id="fromHere" v-model="v$.fromHere.$model")
|
<div class="col-12">
|
||||||
label.form-check-label(for="fromHere") This job was filled due to its listing here
|
<div class="form-check">
|
||||||
template(v-if="expiration.fromHere")
|
<input type="checkbox" id="fromHere" class="form-check-input" v-model="v$.fromHere.$model">
|
||||||
.col-12: p.
|
<label class="form-check-label" for="fromHere">This job was filled due to its listing here</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="expiration.fromHere">
|
||||||
|
<div class="col-12">
|
||||||
|
<p>
|
||||||
Consider telling your fellow citizens about your experience! Comments entered here will be visible to
|
Consider telling your fellow citizens about your experience! Comments entered here will be visible to
|
||||||
logged-on users here, but not to the general public.
|
logged-on users here, but not to the general public.
|
||||||
markdown-editor(id="successStory" label="Your Success Story" v-model:text="v$.successStory.$model")
|
</p>
|
||||||
.col-12
|
</div>
|
||||||
button.btn.btn-primary(@click.prevent="expireListing").
|
<markdown-editor id="successStory" label="Your Success Story" v-model:text="v$.successStory.$model" />
|
||||||
#[icon(:icon="mdiTextBoxRemoveOutline")] Expire Listing
|
</template>
|
||||||
maybe-save(:saveAction="doSave" :validator="v$")
|
<div class="col-12">
|
||||||
|
<button class="btn btn-primary" @click.prevent="expireListing">
|
||||||
|
<icon :icon="mdiTextBoxRemoveOutline" /> Expire Listing
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</load-data>
|
||||||
|
<maybe-save :saveAction="doSave" :validator="v$" />
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -84,7 +97,7 @@ const expireListing = async (navigate : boolean) => {
|
||||||
toastSuccess(`Job Listing Expired${expiration.successStory ? " and Success Story Recorded" : ""} Successfully`)
|
toastSuccess(`Job Listing Expired${expiration.successStory ? " and Success Story Recorded" : ""} Successfully`)
|
||||||
v$.value.$reset()
|
v$.value.$reset()
|
||||||
if (navigate) {
|
if (navigate) {
|
||||||
router.push("/listings/mine")
|
await router.push("/listings/mine")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
load-data(:load="retrieveListing")
|
<load-data :load="retrieveListing">
|
||||||
h3
|
<h3>
|
||||||
| {{it.listing.title}}
|
{{it.listing.title}}
|
||||||
.jjj-heading-label(v-if="it.listing.isExpired")
|
<span v-if="it.listing.isExpired" class="jjj-heading-label">
|
||||||
| #[span.badge.bg-warning.text-dark Expired]
|
<span class="badge bg-warning text-dark">Expired</span>
|
||||||
template(v-if="it.listing.wasFilledHere") #[span.badge.bg-success Filled via Jobs, Jobs, Jobs]
|
<template v-if="it.listing.wasFilledHere">
|
||||||
h4.pb-3.text-muted {{it.continent.name}} / {{it.listing.region}}
|
<span class="badge bg-success">Filled via Jobs, Jobs, Jobs</span>
|
||||||
p
|
</template>
|
||||||
template(v-if="it.listing.neededBy").
|
</span>
|
||||||
#[strong #[em NEEDED BY {{neededBy(it.listing.neededBy)}}]] •
|
</h3>
|
||||||
| Listed by #[a(:href="profileUrl" target="_blank") {{citizenName(citizen)}}]
|
<h4 class="pb-3 text-muted">{{it.continent.name}} / {{it.listing.region}}</h4>
|
||||||
hr
|
<p>
|
||||||
div(v-html="details")
|
<template v-if="it.listing.neededBy">
|
||||||
|
<strong><em>NEEDED BY {{neededBy(it.listing.neededBy)}}</em></strong> •
|
||||||
|
</template>
|
||||||
|
Listed by <!-- a :href="profileUrl" target="_blank" -->{{citizenName(citizen)}}<!-- /a -->
|
||||||
|
</p>
|
||||||
|
<hr>
|
||||||
|
<div v-html="details" />
|
||||||
|
</load-data>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -63,7 +71,7 @@ const retrieveListing = async (errors : string[]) => {
|
||||||
const details = computed(() => toHtml(it.value?.listing.text ?? ""))
|
const details = computed(() => toHtml(it.value?.listing.text ?? ""))
|
||||||
|
|
||||||
/** The Mastodon profile URL for the citizen who posted this job listing */
|
/** The Mastodon profile URL for the citizen who posted this job listing */
|
||||||
const profileUrl = computed(() => citizen.value ? citizen.value.profileUrl : "")
|
// const profileUrl = computed(() => citizen.value ? citizen.value.profileUrl : "")
|
||||||
|
|
||||||
/** The needed by date, formatted in SHOUTING MODE */
|
/** The needed by date, formatted in SHOUTING MODE */
|
||||||
const neededBy = (nb : string) => formatNeededBy(nb).toUpperCase()
|
const neededBy = (nb : string) => formatNeededBy(nb).toUpperCase()
|
||||||
|
|
|
@ -1,42 +1,57 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3.pb-3 My Job Listings
|
<h3 class="pb-3">My Job Listings</h3>
|
||||||
p: router-link.btn.btn-outline-primary(to="/listing/new/edit") Add a New Job Listing
|
<p><router-link class="btn btn-outline-primary" to="/listing/new/edit">Add a New Job Listing</router-link></p>
|
||||||
load-data(:load="getListings")
|
<load-data :load="getListings">
|
||||||
h4.pb-2(v-if="expired.length > 0") Active Job Listings
|
<h4 v-if="expired.length > 0" class="pb-2">Active Job Listings</h4>
|
||||||
table.pb-3.table.table-sm.table-hover.pt-3(v-if="active.length > 0")
|
<table v-if="active.length > 0" class="pb-3 table table-sm table-hover pt-3">
|
||||||
thead: tr
|
<thead>
|
||||||
th(scope="col") Action
|
<tr>
|
||||||
th(scope="col") Title
|
<th scope="col">Action</th>
|
||||||
th(scope="col") Continent / Region
|
<th scope="col">Title</th>
|
||||||
th(scope="col") Created
|
<th scope="col">Continent / Region</th>
|
||||||
th(scope="col") Updated
|
<th scope="col">Created</th>
|
||||||
tbody: tr(v-for="it in active" :key="it.listing.id")
|
<th scope="col">Updated</th>
|
||||||
td
|
</tr>
|
||||||
router-link(:to="`/listing/${it.listing.id}/edit`") Edit
|
</thead>
|
||||||
= " ~ "
|
<tbody>
|
||||||
router-link(:to="`/listing/${it.listing.id}/view`") View
|
<tr v-for="it in active" :key="it.listing.id">
|
||||||
= " ~ "
|
<td>
|
||||||
router-link(:to="`/listing/${it.listing.id}/expire`") Expire
|
<router-link :to="`/listing/${it.listing.id}/edit`">Edit</router-link> ~
|
||||||
td {{it.listing.title}}
|
<router-link :to="`/listing/${it.listing.id}/view`">View</router-link> ~
|
||||||
td {{it.continent.name}} / {{it.listing.region}}
|
<router-link :to="`/listing/${it.listing.id}/expire`">Expire</router-link>
|
||||||
td: full-date-time(:date="it.listing.createdOn")
|
</td>
|
||||||
td: full-date-time(:date="it.listing.updatedOn")
|
<td>{{it.listing.title}}</td>
|
||||||
p.pb-3.fst-italic(v-else) You have no active job listings
|
<td>{{it.continent.name}} / {{it.listing.region}}</td>
|
||||||
template(v-if="expired.length > 0")
|
<td><full-date-time :date="it.listing.createdOn" /></td>
|
||||||
h4.pb-2 Expired Job Listings
|
<td><full-date-time :date="it.listing.updatedOn" /></td>
|
||||||
table.table.table-sm.table-hover.pt-3
|
</tr>
|
||||||
thead: tr
|
</tbody>
|
||||||
th(scope="col") Action
|
</table>
|
||||||
th(scope="col") Title
|
<p v-else class="pb-3 fst-italic">You have no active job listings</p>
|
||||||
th(scope="col") Filled Here?
|
<template v-if="expired.length > 0">
|
||||||
th(scope="col") Expired
|
<h4 class="pb-2">Expired Job Listings</h4>
|
||||||
tbody: tr(v-for="it in expired" :key="it.listing.id")
|
<table class="table table-sm table-hover pt-3">
|
||||||
td
|
<thead>
|
||||||
router-link(:to="`/listing/${it.listing.id}/view`") View
|
<tr>
|
||||||
td {{it.listing.title}}
|
<th scope="col">Action</th>
|
||||||
td {{yesOrNo(it.listing.wasFilledHere)}}
|
<th scope="col">Title</th>
|
||||||
td: full-date-time(:date="it.listing.updatedOn")
|
<th scope="col">Filled Here?</th>
|
||||||
|
<th scope="col">Expired</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="it in expired" :key="it.listing.id">
|
||||||
|
<td><router-link :to="`/listing/${it.listing.id}/view`">View</router-link></td>
|
||||||
|
<td>{{it.listing.title}}</td>
|
||||||
|
<td>{{yesOrNo(it.listing.wasFilledHere)}}</td>
|
||||||
|
<td><full-date-time :date="it.listing.updatedOn" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
</load-data>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
206
src/JobsJobsJobs/App/src/views/profile/EditProfile.vue
Normal file
206
src/JobsJobsJobs/App/src/views/profile/EditProfile.vue
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
<template>
|
||||||
|
<article>
|
||||||
|
<h3 class="pb-3">My Employment Profile</h3>
|
||||||
|
<load-data :load="retrieveData">
|
||||||
|
<form class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" id="isSeeking" class="form-check-input" v-model="v$.isSeekingEmployment.$model">
|
||||||
|
<label class="form-check-label" for="isSeeking">I am currently seeking employment</label>
|
||||||
|
</div>
|
||||||
|
<p v-if="profile.isSeekingEmployment">
|
||||||
|
<em>
|
||||||
|
If you have found employment, consider
|
||||||
|
<router-link to="/success-story/new/edit">telling your fellow citizens about it!</router-link>
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 col-md-4">
|
||||||
|
<continent-list v-model="v$.continentId.$model" :isInvalid="v$.continentId.$error"
|
||||||
|
@touch="v$.continentId.$touch() || true" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 col-md-8">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" id="region" :class="{'form-control': true, 'is-invalid': v$.region.$error }"
|
||||||
|
v-model="v$.region.$model" maxlength="255" placeholder="Country, state, geographic area, etc.">
|
||||||
|
<div class="invalid-feedback">Please enter a region</div>
|
||||||
|
<label class="jjj-required" for="region">Region</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Country, state, geographic area, etc.</div>
|
||||||
|
</div>
|
||||||
|
<markdown-editor id="bio" label="Professional Biography" v-model:text="v$.biography.$model"
|
||||||
|
:isInvalid="v$.biography.$error" />
|
||||||
|
<div class="col-12 col-offset-md-2 col-md-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" id="isRemote" class="form-check-input" v-model="v$.remoteWork.$model">
|
||||||
|
<label class="form-check-label" for="isRemote">I am looking for remote work</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" id="isFullTime" class="form-check-input" v-model="v$.fullTime.$model">
|
||||||
|
<label class="form-check-label" for="isFullTime">I am looking for full-time work</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<hr>
|
||||||
|
<h4 class="pb-2">
|
||||||
|
Skills
|
||||||
|
<button class="btn btn-sm btn-outline-primary.rounded-pill" @click.prevent="addSkill">Add a Skill</button>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<profile-skill-edit v-for="(skill, idx) in profile.skills" :key="skill.id" v-model="profile.skills[idx]"
|
||||||
|
@remove="removeSkill(skill.id)" @input="v$.skills.$touch" />
|
||||||
|
<div class="col-12">
|
||||||
|
<hr>
|
||||||
|
<h4>Experience</h4>
|
||||||
|
<p>
|
||||||
|
This application does not have a place to individually list your chronological job history; however, you
|
||||||
|
can use this area to list prior jobs, their dates, and anything else you want to include that’s not
|
||||||
|
already a part of your Professional Biography above.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<markdown-editor id="experience" label="Experience" v-model:text="v$.experience.$model" />
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" id="isPublic" class="form-check-input" v-model="v$.isPublic.$model">
|
||||||
|
<label class="form-check-label" for="isPublic">Allow my profile to be searched publicly</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<p v-if="v$.$error" class="text-danger">Please correct the errors above</p>
|
||||||
|
<button class="btn btn-primary" @click.prevent="saveProfile">
|
||||||
|
<icon :icon="mdiContentSaveOutline" /> Save
|
||||||
|
</button>
|
||||||
|
<template v-if="!isNew">
|
||||||
|
|
||||||
|
<router-link class="btn btn-outline-secondary" :to="`/profile/${user.citizenId}/view`">
|
||||||
|
<icon color="#6c757d" :icon="mdiFileAccountOutline" /> View Your User Profile
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</load-data>
|
||||||
|
<hr>
|
||||||
|
<p class="text-muted fst-italic">
|
||||||
|
(If you want to delete your profile, or your entire account,
|
||||||
|
<router-link to="/so-long/options">see your deletion options here</router-link>.)
|
||||||
|
</p>
|
||||||
|
<maybe-save :saveAction="saveProfile" :validator="v$" />
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, reactive } from "vue"
|
||||||
|
import { mdiContentSaveOutline, mdiFileAccountOutline } from "@mdi/js"
|
||||||
|
import useVuelidate from "@vuelidate/core"
|
||||||
|
import { required } from "@vuelidate/validators"
|
||||||
|
|
||||||
|
import api, { LogOnSuccess, Profile, ProfileForm } from "@/api"
|
||||||
|
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
|
||||||
|
import { useStore } from "@/store"
|
||||||
|
|
||||||
|
import ContinentList from "@/components/ContinentList.vue"
|
||||||
|
import LoadData from "@/components/LoadData.vue"
|
||||||
|
import MarkdownEditor from "@/components/MarkdownEditor.vue"
|
||||||
|
import MaybeSave from "@/components/MaybeSave.vue"
|
||||||
|
import ProfileSkillEdit from "@/components/profile/SkillEdit.vue"
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
/** The currently logged-on user */
|
||||||
|
const user = store.state.user as LogOnSuccess
|
||||||
|
|
||||||
|
/** Whether this is a new profile */
|
||||||
|
const isNew = ref(false)
|
||||||
|
|
||||||
|
/** The starting values for a new employment profile */
|
||||||
|
const newProfile : Profile = {
|
||||||
|
id: user.citizenId,
|
||||||
|
seekingEmployment: false,
|
||||||
|
isPublic: false,
|
||||||
|
continentId: "",
|
||||||
|
region: "",
|
||||||
|
remoteWork: false,
|
||||||
|
fullTime: false,
|
||||||
|
biography: "",
|
||||||
|
lastUpdatedOn: "",
|
||||||
|
experience: undefined,
|
||||||
|
skills: []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The user's current profile (plus a few items, adapted for editing) */
|
||||||
|
const profile = reactive(new ProfileForm())
|
||||||
|
|
||||||
|
/** The validation rules for the form */
|
||||||
|
const rules = computed(() => ({
|
||||||
|
isSeekingEmployment: { },
|
||||||
|
isPublic: { },
|
||||||
|
continentId: { required },
|
||||||
|
region: { required },
|
||||||
|
remoteWork: { },
|
||||||
|
fullTime: { },
|
||||||
|
biography: { required },
|
||||||
|
experience: { },
|
||||||
|
skills: { }
|
||||||
|
}))
|
||||||
|
|
||||||
|
/** Initialize form validation */
|
||||||
|
const v$ = useVuelidate(rules, profile, { $lazy: true })
|
||||||
|
|
||||||
|
/** Retrieve the user's profile and their real name */
|
||||||
|
const retrieveData = async (errors : string[]) => {
|
||||||
|
const profileResult = await api.profile.retreive(undefined, user)
|
||||||
|
if (typeof profileResult === "string") {
|
||||||
|
errors.push(profileResult)
|
||||||
|
} else if (typeof profileResult === "undefined") {
|
||||||
|
isNew.value = true
|
||||||
|
}
|
||||||
|
const nameResult = await api.citizen.retrieve(user.citizenId, user)
|
||||||
|
if (typeof nameResult === "string") {
|
||||||
|
errors.push(nameResult)
|
||||||
|
}
|
||||||
|
if (errors.length > 0) return
|
||||||
|
// Update the empty form with appropriate values
|
||||||
|
const p = isNew.value ? newProfile : profileResult as Profile
|
||||||
|
profile.isSeekingEmployment = p.seekingEmployment
|
||||||
|
profile.isPublic = p.isPublic
|
||||||
|
profile.continentId = p.continentId
|
||||||
|
profile.region = p.region
|
||||||
|
profile.remoteWork = p.remoteWork
|
||||||
|
profile.fullTime = p.fullTime
|
||||||
|
profile.biography = p.biography
|
||||||
|
profile.experience = p.experience
|
||||||
|
profile.skills = p.skills
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The ID for new skills */
|
||||||
|
let newSkillId = 0
|
||||||
|
|
||||||
|
/** Add a skill to the profile */
|
||||||
|
const addSkill = () => {
|
||||||
|
profile.skills.push({ id: `new${newSkillId++}`, description: "", notes: undefined })
|
||||||
|
v$.value.skills.$touch()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove the given skill from the profile */
|
||||||
|
const removeSkill = (skillId : string) => {
|
||||||
|
profile.skills = profile.skills.filter(s => s.id !== skillId)
|
||||||
|
v$.value.skills.$touch()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save the current profile values */
|
||||||
|
const saveProfile = async () => {
|
||||||
|
v$.value.$touch()
|
||||||
|
if (v$.value.$error) return
|
||||||
|
// Remove any blank skills before submitting
|
||||||
|
profile.skills = profile.skills.filter(s => !(s.description.trim() === "" && (s.notes ?? "").trim() === ""))
|
||||||
|
const saveResult = await api.profile.save(profile, user)
|
||||||
|
if (typeof saveResult === "string") {
|
||||||
|
toastError(saveResult, "saving profile")
|
||||||
|
} else {
|
||||||
|
toastSuccess("Profile Saved Successfully")
|
||||||
|
v$.value.$reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,29 +1,41 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3.pb-3 Search Profiles
|
<h3 class="pb-3">Search Profiles</h3>
|
||||||
p(v-if="!searched").
|
<p v-if="!searched">
|
||||||
Enter one or more criteria to filter results, or just click “Search” to list all profiles.
|
Enter one or more criteria to filter results, or just click “Search” to list all profiles.
|
||||||
collapse-panel(headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse")
|
</p>
|
||||||
profile-search-form(v-model="criteria" @search="doSearch")
|
<collapse-panel headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse">
|
||||||
error-list(:errors="errors")
|
<profile-search-form v-model="criteria" @search="doSearch" />
|
||||||
p.pt-3(v-if="searching") Searching profiles…
|
</collapse-panel>
|
||||||
template(v-else)
|
<error-list :errors="errors">
|
||||||
table.table.table-sm.table-hover.pt-3(v-if="results.length > 0")
|
<p v-if="searching" class="pt-3">Searching profiles…</p>
|
||||||
thead: tr
|
<template v-else>
|
||||||
th(scope="col") Profile
|
<table v-if="results.length > 0" class="table table-sm table-hover pt-3">
|
||||||
th(scope="col") Name
|
<thead>
|
||||||
th.text-center(scope="col" v-if="wideDisplay") Seeking?
|
<tr>
|
||||||
th.text-center(scope="col") Remote?
|
<th scope="col">Profile</th>
|
||||||
th.text-center(scope="col" v-if="wideDisplay") Full-Time?
|
<th scope="col">Name</th>
|
||||||
th(scope="col" v-if="wideDisplay") Last Updated
|
<th v-if="wideDisplay" class="text-center" scope="col">Seeking?</th>
|
||||||
tbody: tr(v-for="profile in results" :key="profile.citzenId")
|
<th class="text-center" scope="col">Remote?</th>
|
||||||
td: router-link(:to="`/profile/${profile.citizenId}/view`") View
|
<th v-if="wideDisplay" class="text-center" scope="col">Full-Time?</th>
|
||||||
td(:class="{ 'fw-bold' : profile.seekingEmployment }") {{profile.displayName}}
|
<th v-if="wideDisplay" scope="col">Last Updated</th>
|
||||||
td.text-center(v-if="wideDisplay") {{yesOrNo(profile.seekingEmployment)}}
|
</tr>
|
||||||
td.text-center {{yesOrNo(profile.remoteWork)}}
|
</thead>
|
||||||
td.text-center(v-if="wideDisplay") {{yesOrNo(profile.fullTime)}}
|
<tbody>
|
||||||
td(v-if="wideDisplay"): full-date(:date="profile.lastUpdatedOn")
|
<tr v-for="profile in results" :key="profile.citzenId">
|
||||||
p.pt-3(v-else-if="searched") No results found for the specified criteria
|
<td><router-link :to="`/profile/${profile.citizenId}/view`">View</router-link></td>
|
||||||
|
<td :class="{ 'fw-bold' : profile.seekingEmployment }">{{profile.displayName}}</td>
|
||||||
|
<td v-if="wideDisplay" class="text-center">{{yesOrNo(profile.seekingEmployment)}}</td>
|
||||||
|
<td class="text-center">{{yesOrNo(profile.remoteWork)}}</td>
|
||||||
|
<td v-if="wideDisplay" class="text-center">{{yesOrNo(profile.fullTime)}}</td>
|
||||||
|
<td v-if="wideDisplay"><full-date :date="profile.lastUpdatedOn" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p v-else-if="searched" class="pt-3">No results found for the specified criteria</p>
|
||||||
|
</template>
|
||||||
|
</error-list>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<load-data :load="retrieveProfile">
|
<load-data :load="retrieveProfile">
|
||||||
<h2>
|
<h2>
|
||||||
<a :href="it.citizen.profileUrl" target="_blank" rel="noopener">{{citizenName(it.citizen)}}</a>
|
<a :href="it.citizen.profileUrl" target="_blank" rel="noopener">{{citizenName(it.citizen)}}</a>
|
||||||
<span class="jjj-heading-label" v-if="it.profile.seekingEmployment">
|
<span v-if="it.profile.seekingEmployment" class="jjj-heading-label">
|
||||||
<span class="badge bg-dark">Currently Seeking Employment</span>
|
<span class="badge bg-dark">Currently Seeking Employment</span>
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-if="user.citizenId === it.citizen.id">
|
<template v-if="user.citizenId === it.citizen.id">
|
||||||
<br><br>
|
<br><br>
|
||||||
<router-link class="btn btn-primary" to="/citizen/profile">
|
<router-link class="btn btn-primary" to="/profile/edit">
|
||||||
<icon :icon="mdiPencil" /> Edit Your Profile
|
<icon :icon="mdiPencil" /> Edit Your Profile
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,29 +1,43 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3.pb-3 People Seeking Work
|
<h3 class="pb-3">People Seeking Work</h3>
|
||||||
p(v-if="!searched").
|
<p v-if="!searched">
|
||||||
Enter one or more criteria to filter results, or just click “Search” to list all profiles.
|
Enter one or more criteria to filter results, or just click “Search” to list all profiles.
|
||||||
collapse-panel(headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse")
|
</p>
|
||||||
profile-public-search-form(v-model="criteria" @search="doSearch")
|
<collapse-panel headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse">
|
||||||
error-list(:errors="errors")
|
<profile-public-search-form v-model="criteria" @search="doSearch" />
|
||||||
p(v-if="searching") Searching profiles…
|
</collapse-panel>
|
||||||
template(v-else)
|
<error-list :errors="errors">
|
||||||
template(v-if="results.length > 0")
|
<p v-if="searching">Searching profiles…</p>
|
||||||
p.pb-3.pt-3.
|
<template v-else>
|
||||||
|
<template v-if="results.length > 0">
|
||||||
|
<p class="py-3">
|
||||||
These profiles match your search criteria. To learn more about these people, join the merry band of human
|
These profiles match your search criteria. To learn more about these people, join the merry band of human
|
||||||
resources in the #[a(href="https://noagendashow.net" target="_blank") No Agenda] tribe!
|
resources in the <a href="https://noagendashow.net" target="_blank" rel="noopener">No Agenda</a> tribe!
|
||||||
table.table.table-sm.table-hover
|
</p>
|
||||||
thead: tr
|
<table class="table table-sm table-hover">
|
||||||
th(scope="col") Continent
|
<thead>
|
||||||
th.text-center(scope="col") Region
|
<tr>
|
||||||
th.text-center(scope="col") Remote?
|
<th scope="col">Continent</th>
|
||||||
th.text-center(scope="col") Skills
|
<th class="text-center" scope="col">Region</th>
|
||||||
tbody: tr(v-for="(profile, idx) in results" :key="idx")
|
<th class="text-center" scope="col">Remote?</th>
|
||||||
td {{profile.continent}}
|
<th class="text-center" scope="col">Skills</th>
|
||||||
td {{profile.region}}
|
</tr>
|
||||||
td.text-center {{yesOrNo(profile.remoteWork)}}
|
</thead>
|
||||||
td: template(v-for="(skill, idx) in profile.skills" :key="idx") {{skill}}#[br]
|
<tbody>
|
||||||
p.pt-3(v-else-if="searched") No results found for the specified criteria
|
<tr v-for="(profile, idx) in results" :key="idx">
|
||||||
|
<td>{{profile.continent}}</td>
|
||||||
|
<td>{{profile.region}}</td>
|
||||||
|
<td class="text-center">{{yesOrNo(profile.remoteWork)}}</td>
|
||||||
|
<td><template v-for="(skill, idx) in profile.skills" :key="idx">{{skill}}<br></template></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
<p v-else-if="searched" class="pt-3">No results found for the specified criteria</p>
|
||||||
|
</template>
|
||||||
|
</error-list>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<article>
|
<article>
|
||||||
<h3 class="pb-3">Success Stories</h3>
|
<h3 class="pb-3">Success Stories</h3>
|
||||||
<load-data :load="retrieveStories">
|
<load-data :load="retrieveStories">
|
||||||
<table class="table table-sm table-hover" v-if="stories?.length > 0">
|
<table v-if="stories?.length > 0" class="table table-sm table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Story</th>
|
<th scope="col">Story</th>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<load-data :load="retrieveStory">
|
<load-data :load="retrieveStory">
|
||||||
<h3>
|
<h3>
|
||||||
{{citizenName}}’s Success Story
|
{{citizenName}}’s Success Story
|
||||||
<span class="jjj-heading-label" v-if="story.fromHere">
|
<span v-if="story.fromHere" class="jjj-heading-label">
|
||||||
<span class="badge bg-success">Via {{profileOrListing}} on Jobs, Jobs, Jobs</span>
|
<span class="badge bg-success">Via {{profileOrListing}} on Jobs, Jobs, Jobs</span>
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -34,7 +34,7 @@ const user = store.state.user as LogOnSuccess
|
||||||
/** The story to be displayed */
|
/** The story to be displayed */
|
||||||
const story : Ref<Success | undefined> = ref(undefined)
|
const story : Ref<Success | undefined> = ref(undefined)
|
||||||
|
|
||||||
/** The citizen's name (real, display, or Mastodon, whichever is found first) */
|
/** The citizen's name */
|
||||||
const citizenName = ref("")
|
const citizenName = ref("")
|
||||||
|
|
||||||
/** Retrieve the success story */
|
/** Retrieve the success story */
|
||||||
|
|
|
@ -47,7 +47,8 @@ module DataConnection =
|
||||||
/// Create tables
|
/// Create tables
|
||||||
let private createTables () = backgroundTask {
|
let private createTables () = backgroundTask {
|
||||||
let sql = [
|
let sql = [
|
||||||
$"CREATE SCHEMA IF NOT EXISTS jjj"
|
"CREATE SCHEMA IF NOT EXISTS jjj"
|
||||||
|
// Tables
|
||||||
$"CREATE TABLE IF NOT EXISTS {Table.Citizen} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)"
|
$"CREATE TABLE IF NOT EXISTS {Table.Citizen} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)"
|
||||||
$"CREATE TABLE IF NOT EXISTS {Table.Continent} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)"
|
$"CREATE TABLE IF NOT EXISTS {Table.Continent} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)"
|
||||||
$"CREATE TABLE IF NOT EXISTS {Table.Listing} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)"
|
$"CREATE TABLE IF NOT EXISTS {Table.Listing} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)"
|
||||||
|
@ -56,11 +57,12 @@ module DataConnection =
|
||||||
$"CREATE TABLE IF NOT EXISTS {Table.SecurityInfo} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL,
|
$"CREATE TABLE IF NOT EXISTS {Table.SecurityInfo} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL,
|
||||||
CONSTRAINT fk_security_info_citizen FOREIGN KEY (id) REFERENCES {Table.Citizen} (id) ON DELETE CASCADE)"
|
CONSTRAINT fk_security_info_citizen FOREIGN KEY (id) REFERENCES {Table.Citizen} (id) ON DELETE CASCADE)"
|
||||||
$"CREATE TABLE IF NOT EXISTS {Table.Success} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)"
|
$"CREATE TABLE IF NOT EXISTS {Table.Success} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)"
|
||||||
$"CREATE INDEX IF NOT EXISTS idx_citizen_email ON {Table.Citizen} USING GIN ((data -> 'email'))"
|
// Key indexes
|
||||||
$"CREATE INDEX IF NOT EXISTS idx_listing_citizen ON {Table.Listing} USING GIN ((data -> 'citizenId'))"
|
$"CREATE UNIQUE INDEX IF NOT EXISTS uk_citizen_email ON {Table.Citizen} ((data -> 'email'))"
|
||||||
$"CREATE INDEX IF NOT EXISTS idx_listing_continent ON {Table.Listing} USING GIN ((data -> 'continentId'))"
|
$"CREATE INDEX IF NOT EXISTS idx_listing_citizen ON {Table.Listing} ((data -> 'citizenId'))"
|
||||||
$"CREATE INDEX IF NOT EXISTS idx_profile_continent ON {Table.Profile} USING GIN ((data -> 'continentId'))"
|
$"CREATE INDEX IF NOT EXISTS idx_listing_continent ON {Table.Listing} ((data -> 'continentId'))"
|
||||||
$"CREATE INDEX IF NOT EXISTS idx_success_citizen ON {Table.Success} USING GIN ((data -> 'citizenId'))"
|
$"CREATE INDEX IF NOT EXISTS idx_profile_continent ON {Table.Profile} ((data -> 'continentId'))"
|
||||||
|
$"CREATE INDEX IF NOT EXISTS idx_success_citizen ON {Table.Success} ((data -> 'citizenId'))"
|
||||||
]
|
]
|
||||||
let! _ =
|
let! _ =
|
||||||
connection ()
|
connection ()
|
||||||
|
@ -207,15 +209,21 @@ module Citizens =
|
||||||
let save citizen =
|
let save citizen =
|
||||||
saveCitizen citizen (connection ())
|
saveCitizen citizen (connection ())
|
||||||
|
|
||||||
/// Register a citizen (saves citizen and security settings)
|
/// Register a citizen (saves citizen and security settings); returns false if the e-mail is already taken
|
||||||
let register citizen (security : SecurityInfo) = backgroundTask {
|
let register citizen (security : SecurityInfo) = backgroundTask {
|
||||||
let connProps = connection ()
|
let connProps = connection ()
|
||||||
use conn = Sql.createConnection connProps
|
use conn = Sql.createConnection connProps
|
||||||
do! conn.OpenAsync ()
|
do! conn.OpenAsync ()
|
||||||
use! txn = conn.BeginTransactionAsync ()
|
use! txn = conn.BeginTransactionAsync ()
|
||||||
|
try
|
||||||
do! saveCitizen citizen connProps
|
do! saveCitizen citizen connProps
|
||||||
do! saveSecurity security connProps
|
do! saveSecurity security connProps
|
||||||
do! txn.CommitAsync ()
|
do! txn.CommitAsync ()
|
||||||
|
return true
|
||||||
|
with
|
||||||
|
| :? Npgsql.PostgresException as ex when ex.SqlState = "23505" && ex.ConstraintName = "uk_citizen_email" ->
|
||||||
|
do! txn.RollbackAsync ()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to find the security information matching a confirmation token
|
/// Try to find the security information matching a confirmation token
|
||||||
|
@ -256,7 +264,8 @@ module Citizens =
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt a user log on
|
/// Attempt a user log on
|
||||||
let tryLogOn email (pwCheck : string -> bool) now = backgroundTask {
|
let tryLogOn email password (pwVerify : Citizen -> string -> bool option) (pwHash : Citizen -> string -> string)
|
||||||
|
now = backgroundTask {
|
||||||
do! checkForPurge false
|
do! checkForPurge false
|
||||||
let connProps = connection ()
|
let connProps = connection ()
|
||||||
let! tryCitizen =
|
let! tryCitizen =
|
||||||
|
@ -281,11 +290,14 @@ module Citizens =
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
if info.AccountLocked then return Error "Log on unsuccessful (Account Locked)"
|
if info.AccountLocked then return Error "Log on unsuccessful (Account Locked)"
|
||||||
elif pwCheck citizen.PasswordHash then
|
|
||||||
do! saveSecurity { info with FailedLogOnAttempts = 0 } connProps
|
|
||||||
do! saveCitizen { citizen with LastSeenOn = now } connProps
|
|
||||||
return Ok { citizen with LastSeenOn = now }
|
|
||||||
else
|
else
|
||||||
|
match pwVerify citizen password with
|
||||||
|
| Some rehash ->
|
||||||
|
let hash = if rehash then pwHash citizen password else citizen.PasswordHash
|
||||||
|
do! saveSecurity { info with FailedLogOnAttempts = 0 } connProps
|
||||||
|
do! saveCitizen { citizen with LastSeenOn = now; PasswordHash = hash } connProps
|
||||||
|
return Ok { citizen with LastSeenOn = now }
|
||||||
|
| None ->
|
||||||
let locked = info.FailedLogOnAttempts >= 4
|
let locked = info.FailedLogOnAttempts >= 4
|
||||||
do! { info with FailedLogOnAttempts = info.FailedLogOnAttempts + 1; AccountLocked = locked }
|
do! { info with FailedLogOnAttempts = info.FailedLogOnAttempts + 1; AccountLocked = locked }
|
||||||
|> saveSecurity <| connProps
|
|> saveSecurity <| connProps
|
||||||
|
|
|
@ -90,6 +90,16 @@ type ListingSearch =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// The fields needed to log on to Jobs, Jobs, Jobs
|
||||||
|
type LogOnForm =
|
||||||
|
{ /// The e-mail address for the citizen
|
||||||
|
email : string
|
||||||
|
|
||||||
|
/// The password provided by the user
|
||||||
|
password : string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A successful logon
|
/// A successful logon
|
||||||
type LogOnSuccess =
|
type LogOnSuccess =
|
||||||
{ /// The JSON Web Token (JWT) to use for API access
|
{ /// The JSON Web Token (JWT) to use for API access
|
||||||
|
@ -110,69 +120,16 @@ type Count =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// An instance of a Mastodon server which is configured to work with Jobs, Jobs, Jobs
|
|
||||||
type MastodonInstance () =
|
|
||||||
|
|
||||||
/// The name of the instance
|
|
||||||
member val Name = "" with get, set
|
|
||||||
|
|
||||||
/// The URL for this instance
|
|
||||||
member val Url = "" with get, set
|
|
||||||
|
|
||||||
/// The abbreviation used in the URL to distinguish this instance's return codes
|
|
||||||
member val Abbr = "" with get, set
|
|
||||||
|
|
||||||
/// The client ID (assigned by the Mastodon server)
|
|
||||||
member val ClientId = "" with get, set
|
|
||||||
|
|
||||||
/// The cryptographic secret (provided by the Mastodon server)
|
|
||||||
member val Secret = "" with get, set
|
|
||||||
|
|
||||||
/// Whether the instance is currently enabled
|
|
||||||
member val IsEnabled = true with get, set
|
|
||||||
|
|
||||||
/// If an instance is disabled, the reason for it being disabled
|
|
||||||
member val Reason = "" with get, set
|
|
||||||
|
|
||||||
|
|
||||||
/// The authorization options for Jobs, Jobs, Jobs
|
/// The authorization options for Jobs, Jobs, Jobs
|
||||||
type AuthOptions () =
|
type AuthOptions () =
|
||||||
|
|
||||||
/// The host for the return URL for Mastodon verification
|
/// The secret with which the server signs the JWTs it issues once a user logs on
|
||||||
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
|
member val ServerSecret = "" with get, set
|
||||||
|
|
||||||
/// The instances configured for use
|
|
||||||
member val Instances = Array.empty<MastodonInstance> with get, set
|
|
||||||
|
|
||||||
interface IOptions<AuthOptions> with
|
interface IOptions<AuthOptions> with
|
||||||
override this.Value = this
|
override this.Value = this
|
||||||
|
|
||||||
|
|
||||||
/// The Mastodon instance data provided via the Jobs, Jobs, Jobs API
|
|
||||||
type Instance =
|
|
||||||
{ /// The name of the instance
|
|
||||||
name : string
|
|
||||||
|
|
||||||
/// The URL for this instance
|
|
||||||
url : string
|
|
||||||
|
|
||||||
/// The abbreviation used in the URL to distinguish this instance's return codes
|
|
||||||
abbr : string
|
|
||||||
|
|
||||||
/// The client ID (assigned by the Mastodon server)
|
|
||||||
clientId : string
|
|
||||||
|
|
||||||
/// Whether this instance is currently enabled
|
|
||||||
isEnabled : bool
|
|
||||||
|
|
||||||
/// If not enabled, the reason the instance is disabled
|
|
||||||
reason : string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// The fields required for a skill
|
/// The fields required for a skill
|
||||||
type SkillForm =
|
type SkillForm =
|
||||||
{ /// The ID of this skill
|
{ /// The ID of this skill
|
||||||
|
@ -194,9 +151,6 @@ type ProfileForm =
|
||||||
/// Whether this profile should appear in the public search
|
/// Whether this profile should appear in the public search
|
||||||
isPublic : bool
|
isPublic : bool
|
||||||
|
|
||||||
/// The user's real name
|
|
||||||
realName : string
|
|
||||||
|
|
||||||
/// The ID of the continent on which the citizen is located
|
/// The ID of the continent on which the citizen is located
|
||||||
continentId : string
|
continentId : string
|
||||||
|
|
||||||
|
@ -226,7 +180,6 @@ module ProfileForm =
|
||||||
let fromProfile (profile : Profile) =
|
let fromProfile (profile : Profile) =
|
||||||
{ isSeekingEmployment = profile.IsSeekingEmployment
|
{ isSeekingEmployment = profile.IsSeekingEmployment
|
||||||
isPublic = profile.IsPubliclySearchable
|
isPublic = profile.IsPubliclySearchable
|
||||||
realName = ""
|
|
||||||
continentId = string profile.ContinentId
|
continentId = string profile.ContinentId
|
||||||
region = profile.Region
|
region = profile.Region
|
||||||
remoteWork = profile.IsRemote
|
remoteWork = profile.IsRemote
|
||||||
|
|
|
@ -1,81 +1,7 @@
|
||||||
/// Authorization / authentication functions
|
/// Authorization / authentication functions
|
||||||
module JobsJobsJobs.Api.Auth
|
module JobsJobsJobs.Api.Auth
|
||||||
|
|
||||||
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 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
|
|
||||||
[<JsonPropertyName "acct">]
|
|
||||||
member val AccountName = "" with get, set
|
|
||||||
/// The user's display name as it currently shows on Mastodon
|
|
||||||
[<JsonPropertyName "display_name">]
|
|
||||||
member val DisplayName = "" with get, set
|
|
||||||
/// The user's profile URL
|
|
||||||
[<JsonPropertyName "url">]
|
|
||||||
member val Url = "" with get, set
|
|
||||||
|
|
||||||
|
|
||||||
open Microsoft.Extensions.Logging
|
|
||||||
open System
|
open System
|
||||||
open System.Net.Http
|
|
||||||
open System.Net.Http.Headers
|
|
||||||
open System.Net.Http.Json
|
|
||||||
open System.Text.Json
|
|
||||||
open JobsJobsJobs.Domain.SharedTypes
|
|
||||||
|
|
||||||
/// HTTP client to use to communication with Mastodon
|
|
||||||
let private http =
|
|
||||||
let h = new HttpClient ()
|
|
||||||
h.Timeout <- TimeSpan.FromSeconds 30.
|
|
||||||
h
|
|
||||||
|
|
||||||
/// Verify the authorization code with Mastodon and get the user's profile
|
|
||||||
let verifyWithMastodon (authCode : string) (inst : MastodonInstance) rtnHost (log : ILogger) = task {
|
|
||||||
|
|
||||||
// Function to create a URL for the given instance
|
|
||||||
let apiUrl = sprintf "%s/api/v1/%s" inst.Url
|
|
||||||
|
|
||||||
// Use authorization code to get an access token from Mastodon
|
|
||||||
use! codeResult =
|
|
||||||
http.PostAsJsonAsync ($"{inst.Url}/oauth/token",
|
|
||||||
{| client_id = inst.ClientId
|
|
||||||
client_secret = inst.Secret
|
|
||||||
redirect_uri = $"{rtnHost}/citizen/{inst.Abbr}/authorized"
|
|
||||||
grant_type = "authorization_code"
|
|
||||||
code = authCode
|
|
||||||
scope = "read"
|
|
||||||
|})
|
|
||||||
match codeResult.IsSuccessStatusCode with
|
|
||||||
| true ->
|
|
||||||
let! responseBytes = codeResult.Content.ReadAsByteArrayAsync ()
|
|
||||||
use tokenResponse = JsonSerializer.Deserialize<JsonDocument> (ReadOnlySpan<byte> responseBytes)
|
|
||||||
match tokenResponse with
|
|
||||||
| null -> return Error "Could not parse authorization code result"
|
|
||||||
| _ ->
|
|
||||||
// Use access token to get profile from NAS
|
|
||||||
use req = new HttpRequestMessage (HttpMethod.Get, apiUrl "accounts/verify_credentials")
|
|
||||||
req.Headers.Authorization <- AuthenticationHeaderValue
|
|
||||||
("Bearer", tokenResponse.RootElement.GetProperty("access_token").GetString ())
|
|
||||||
use! profileResult = http.SendAsync req
|
|
||||||
|
|
||||||
match profileResult.IsSuccessStatusCode with
|
|
||||||
| true ->
|
|
||||||
let! profileBytes = profileResult.Content.ReadAsByteArrayAsync ()
|
|
||||||
match JsonSerializer.Deserialize<MastodonAccount>(ReadOnlySpan<byte> profileBytes) with
|
|
||||||
| null -> return Error "Could not parse profile result"
|
|
||||||
| profile -> return Ok profile
|
|
||||||
| false -> return Error $"Could not get profile ({profileResult.StatusCode:D}: {profileResult.ReasonPhrase})"
|
|
||||||
| false ->
|
|
||||||
let! err = codeResult.Content.ReadAsStringAsync ()
|
|
||||||
log.LogError $"Could not get token result from Mastodon:\n {err}"
|
|
||||||
return Error $"Could not get token ({codeResult.StatusCode:D}: {codeResult.ReasonPhrase})"
|
|
||||||
}
|
|
||||||
|
|
||||||
open System.Text
|
open System.Text
|
||||||
open JobsJobsJobs.Domain
|
open JobsJobsJobs.Domain
|
||||||
|
|
||||||
|
@ -84,9 +10,30 @@ let createToken (citizen : Citizen) =
|
||||||
Convert.ToBase64String (Guid.NewGuid().ToByteArray () |> Array.append (Encoding.UTF8.GetBytes citizen.Email))
|
Convert.ToBase64String (Guid.NewGuid().ToByteArray () |> Array.append (Encoding.UTF8.GetBytes citizen.Email))
|
||||||
|
|
||||||
|
|
||||||
open Microsoft.IdentityModel.Tokens
|
/// Password hashing and verification
|
||||||
|
module Passwords =
|
||||||
|
|
||||||
|
open Microsoft.AspNetCore.Identity
|
||||||
|
|
||||||
|
/// The password hasher to use for the application
|
||||||
|
let private hasher = PasswordHasher<Citizen> ()
|
||||||
|
|
||||||
|
/// Hash a password for a user
|
||||||
|
let hash citizen password =
|
||||||
|
hasher.HashPassword (citizen, password)
|
||||||
|
|
||||||
|
/// Verify a password (returns true if the password needs to be rehashed)
|
||||||
|
let verify citizen password =
|
||||||
|
match hasher.VerifyHashedPassword (citizen, citizen.PasswordHash, password) with
|
||||||
|
| PasswordVerificationResult.Success -> Some false
|
||||||
|
| PasswordVerificationResult.SuccessRehashNeeded -> Some true
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
|
|
||||||
open System.IdentityModel.Tokens.Jwt
|
open System.IdentityModel.Tokens.Jwt
|
||||||
open System.Security.Claims
|
open System.Security.Claims
|
||||||
|
open Microsoft.IdentityModel.Tokens
|
||||||
|
open JobsJobsJobs.Domain.SharedTypes
|
||||||
|
|
||||||
/// Create a JSON Web Token for this citizen to use for further requests to this API
|
/// Create a JSON Web Token for this citizen to use for further requests to this API
|
||||||
let createJwt (citizen : Citizen) (cfg : AuthOptions) =
|
let createJwt (citizen : Citizen) (cfg : AuthOptions) =
|
||||||
|
|
|
@ -6,7 +6,6 @@ open JobsJobsJobs.Domain
|
||||||
open JobsJobsJobs.Domain.SharedTypes
|
open JobsJobsJobs.Domain.SharedTypes
|
||||||
open Microsoft.AspNetCore.Http
|
open Microsoft.AspNetCore.Http
|
||||||
open Microsoft.Extensions.Logging
|
open Microsoft.Extensions.Logging
|
||||||
open NodaTime
|
|
||||||
|
|
||||||
/// Handler to return the files required for the Vue client app
|
/// Handler to return the files required for the Vue client app
|
||||||
module Vue =
|
module Vue =
|
||||||
|
@ -50,12 +49,13 @@ module Error =
|
||||||
clearResponse >=> ServerErrors.INTERNAL_ERROR ex.Message
|
clearResponse >=> ServerErrors.INTERNAL_ERROR ex.Message
|
||||||
|
|
||||||
|
|
||||||
|
open NodaTime
|
||||||
|
|
||||||
/// Helper functions
|
/// Helper functions
|
||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
module Helpers =
|
module Helpers =
|
||||||
|
|
||||||
open System.Security.Claims
|
open System.Security.Claims
|
||||||
open NodaTime
|
|
||||||
open Microsoft.Extensions.Configuration
|
open Microsoft.Extensions.Configuration
|
||||||
open Microsoft.Extensions.Options
|
open Microsoft.Extensions.Options
|
||||||
|
|
||||||
|
@ -103,8 +103,6 @@ open JobsJobsJobs.Data
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Citizen =
|
module Citizen =
|
||||||
|
|
||||||
open Microsoft.AspNetCore.Identity
|
|
||||||
|
|
||||||
// POST: /api/citizen/register
|
// POST: /api/citizen/register
|
||||||
let register : HttpHandler = fun next ctx -> task {
|
let register : HttpHandler = fun next ctx -> task {
|
||||||
let! form = ctx.BindJsonAsync<CitizenRegistrationForm> ()
|
let! form = ctx.BindJsonAsync<CitizenRegistrationForm> ()
|
||||||
|
@ -122,7 +120,7 @@ module Citizen =
|
||||||
JoinedOn = now
|
JoinedOn = now
|
||||||
LastSeenOn = now
|
LastSeenOn = now
|
||||||
}
|
}
|
||||||
let citizen = { noPass with PasswordHash = PasswordHasher().HashPassword (noPass, form.Password) }
|
let citizen = { noPass with PasswordHash = Auth.Passwords.hash noPass form.Password }
|
||||||
let security =
|
let security =
|
||||||
{ SecurityInfo.empty with
|
{ SecurityInfo.empty with
|
||||||
Id = citizen.Id
|
Id = citizen.Id
|
||||||
|
@ -131,12 +129,15 @@ module Citizen =
|
||||||
TokenUsage = Some "confirm"
|
TokenUsage = Some "confirm"
|
||||||
TokenExpires = Some (now + (Duration.FromDays 3))
|
TokenExpires = Some (now + (Duration.FromDays 3))
|
||||||
}
|
}
|
||||||
do! Citizens.register citizen security
|
let! success = Citizens.register citizen security
|
||||||
|
if success then
|
||||||
let! emailResponse = Email.sendAccountConfirmation citizen security
|
let! emailResponse = Email.sendAccountConfirmation citizen security
|
||||||
let logFac = logger ctx
|
let logFac = logger ctx
|
||||||
let log = logFac.CreateLogger "JobsJobsJobs.Api.Handlers.Citizen"
|
let log = logFac.CreateLogger "JobsJobsJobs.Api.Handlers.Citizen"
|
||||||
log.LogInformation $"Confirmation e-mail for {citizen.Email} received {emailResponse}"
|
log.LogInformation $"Confirmation e-mail for {citizen.Email} received {emailResponse}"
|
||||||
return! ok next ctx
|
return! ok next ctx
|
||||||
|
else
|
||||||
|
return! RequestErrors.CONFLICT "" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: /api/citizen/confirm
|
// PATCH: /api/citizen/confirm
|
||||||
|
@ -153,9 +154,10 @@ module Citizen =
|
||||||
return! json {| valid = valid |} next ctx
|
return! json {| valid = valid |} next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: /api/citizen/log-on/[code]
|
// POST: /api/citizen/log-on
|
||||||
let logOn (abbr, authCode) : HttpHandler = fun next ctx -> task {
|
let logOn : HttpHandler = fun next ctx -> task {
|
||||||
match! Citizens.tryLogOn "to@do.com" (fun _ -> false) (now ctx) with
|
let! form = ctx.BindJsonAsync<LogOnForm> ()
|
||||||
|
match! Citizens.tryLogOn form.email form.password Auth.Passwords.verify Auth.Passwords.hash (now ctx) with
|
||||||
| Ok citizen ->
|
| Ok citizen ->
|
||||||
return!
|
return!
|
||||||
json
|
json
|
||||||
|
@ -163,51 +165,7 @@ module Citizen =
|
||||||
citizenId = CitizenId.toString citizen.Id
|
citizenId = CitizenId.toString citizen.Id
|
||||||
name = Citizen.name citizen
|
name = Citizen.name citizen
|
||||||
} next ctx
|
} next ctx
|
||||||
| Error msg ->
|
| Error msg -> return! RequestErrors.BAD_REQUEST msg next ctx
|
||||||
// TODO: return error message
|
|
||||||
return! RequestErrors.BAD_REQUEST msg next ctx
|
|
||||||
// Step 1 - Verify with Mastodon
|
|
||||||
// let cfg = authConfig ctx
|
|
||||||
//
|
|
||||||
// match cfg.Instances |> Array.tryFind (fun it -> it.Abbr = abbr) with
|
|
||||||
// | Some instance ->
|
|
||||||
// let log = (logger ctx).CreateLogger (nameof JobsJobsJobs.Api.Auth)
|
|
||||||
//
|
|
||||||
// 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.findByMastodonUser instance.Abbr account.Username dbConn with
|
|
||||||
// | None ->
|
|
||||||
// let it : Citizen =
|
|
||||||
// { 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
|
|
||||||
// | Some citizen ->
|
|
||||||
// let it = { citizen with displayName = noneIfEmpty account.DisplayName; lastSeenOn = now }
|
|
||||||
// do! Data.Citizen.logOnUpdate it dbConn
|
|
||||||
// return it
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Step 3 - Generate JWT
|
|
||||||
// return!
|
|
||||||
// json
|
|
||||||
// { jwt = Auth.createJwt citizen cfg
|
|
||||||
// citizenId = CitizenId.toString citizen.id
|
|
||||||
// name = Citizen.name citizen
|
|
||||||
// } next ctx
|
|
||||||
// | Error err -> return! RequestErrors.BAD_REQUEST err next ctx
|
|
||||||
// | None -> return! Error.notFound next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: /api/citizen/[id]
|
// GET: /api/citizen/[id]
|
||||||
|
@ -235,32 +193,10 @@ module Continent =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handlers for /api/instances routes
|
|
||||||
[<RequireQualifiedAccess>]
|
|
||||||
module Instances =
|
|
||||||
|
|
||||||
/// Convert a Mastodon instance to the one we use in the API
|
|
||||||
let private toInstance (inst : MastodonInstance) =
|
|
||||||
{ name = inst.Name
|
|
||||||
url = inst.Url
|
|
||||||
abbr = inst.Abbr
|
|
||||||
clientId = inst.ClientId
|
|
||||||
isEnabled = inst.IsEnabled
|
|
||||||
reason = inst.Reason
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET: /api/instances
|
|
||||||
let all : HttpHandler = fun next ctx -> task {
|
|
||||||
return! json ((authConfig ctx).Instances |> Array.map toInstance) next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Handlers for /api/listing[s] routes
|
/// Handlers for /api/listing[s] routes
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Listing =
|
module Listing =
|
||||||
|
|
||||||
open NodaTime
|
|
||||||
|
|
||||||
/// Parse the string we receive from JSON into a NodaTime local date
|
/// Parse the string we receive from JSON into a NodaTime local date
|
||||||
let private parseDate = DateTime.Parse >> LocalDate.FromDateTime
|
let private parseDate = DateTime.Parse >> LocalDate.FromDateTime
|
||||||
|
|
||||||
|
@ -510,19 +446,18 @@ open Giraffe.EndpointRouting
|
||||||
let allEndpoints = [
|
let allEndpoints = [
|
||||||
subRoute "/api" [
|
subRoute "/api" [
|
||||||
subRoute "/citizen" [
|
subRoute "/citizen" [
|
||||||
GET_HEAD [
|
GET_HEAD [ routef "/%O" Citizen.get ]
|
||||||
routef "/log-on/%s/%s" Citizen.logOn
|
|
||||||
routef "/%O" Citizen.get
|
|
||||||
]
|
|
||||||
PATCH [ route "/confirm" Citizen.confirmToken ]
|
PATCH [ route "/confirm" Citizen.confirmToken ]
|
||||||
POST [ route "/register" Citizen.register ]
|
POST [
|
||||||
|
route "/log-on" Citizen.logOn
|
||||||
|
route "/register" Citizen.register
|
||||||
|
]
|
||||||
DELETE [
|
DELETE [
|
||||||
route "" Citizen.delete
|
route "" Citizen.delete
|
||||||
route "/deny" Citizen.denyToken
|
route "/deny" Citizen.denyToken
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
GET_HEAD [ route "/continents" Continent.all ]
|
GET_HEAD [ route "/continents" Continent.all ]
|
||||||
GET_HEAD [ route "/instances" Instances.all ]
|
|
||||||
subRoute "/listing" [
|
subRoute "/listing" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
routef "/%O" Listing.get
|
routef "/%O" Listing.get
|
||||||
|
|
|
@ -1,28 +1,8 @@
|
||||||
{
|
{
|
||||||
"Auth": {
|
"Logging": {
|
||||||
"ReturnHost": "http://localhost:5000",
|
"LogLevel": {
|
||||||
"Instances": {
|
"JobsJobsJobs.Api.Handlers.Citizen": "Information",
|
||||||
"0": {
|
"Microsoft.AspNetCore.StaticFiles": "Warning"
|
||||||
"Name": "No Agenda Social",
|
|
||||||
"Url": "https://noagendasocial.com",
|
|
||||||
"Abbr": "nas",
|
|
||||||
"IsEnabled": true,
|
|
||||||
"Reason": ""
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
"Name": "ITM Slaves!",
|
|
||||||
"Url": "https://itmslaves.com",
|
|
||||||
"Abbr": "itm",
|
|
||||||
"IsEnabled": false,
|
|
||||||
"Reason": "This site has changed platforms, and its integration is not yet restored"
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"Name": "Liberty Woof",
|
|
||||||
"Url": "https://libertywoof.com",
|
|
||||||
"Abbr": "lw",
|
|
||||||
"IsEnabled": false,
|
|
||||||
"Reason": "This site may have gone away; it is currently inaccessible"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user