Compare commits

...

19 Commits

Author SHA1 Message Date
Keith Irwin 4bf9724794
Commented stuff out 2022-09-28 20:15:26 -06:00
Keith Irwin 621b77e715
Finished new invoice code 2022-02-18 12:28:38 -07:00
Keith Irwin a5f3be4507
Final tweaks on new invoice page 2022-02-17 11:19:26 -07:00
Keith Irwin 4fdecc8cc1
Fixed adding instances without setting end date 2022-02-16 17:25:50 -07:00
Keith Irwin 008d6dc145
replaced parseFloat() with Number() 2022-02-15 11:19:28 -07:00
Keith Irwin 88b3a69ec7
Added new invoice functionality 2022-02-10 13:00:53 -07:00
Keith Irwin be8fd8ff82
Items for invoices 2022-02-08 12:16:25 -07:00
Keith Irwin b3f876f456
Added invoicing 2022-02-03 13:15:41 -07:00
Keith Irwin 22387acc60
Fixed dashboard 2022-01-30 18:35:34 -07:00
Keith Irwin 77ae645b19
Major fixes, can now update instances 2022-01-29 19:52:43 -07:00
Keith Irwin 379864737b
Strip local storage if not logged in. If logged in, redirect from /login/ 2022-01-21 09:56:53 -07:00
Keith Irwin 91707d7ba4
Fixes to login and admin dashboard 2022-01-21 09:50:44 -07:00
Keith Irwin fcbcda4e07
Upgraded to searXNG 2022-01-18 17:28:00 -07:00
Keith Irwin a8414f7b56
Fixed flexbox on web search 2022-01-18 14:13:41 -07:00
Keith Irwin 057977ed15
Fixed button size issue 2022-01-18 14:09:52 -07:00
Keith Irwin b3916c66e8
Moved websearch and speedtest into site 2022-01-18 14:08:26 -07:00
Keith Irwin 1da582aa81
Fixed logout 2022-01-18 11:22:03 -07:00
Keith Irwin c4df6a6f6b
Fixed login/logout button 2022-01-18 10:31:32 -07:00
Keith Irwin 2a81b27cb1
Got basic login/dashboard/admin working 2022-01-15 16:13:28 -07:00
24 changed files with 2518 additions and 71 deletions

View File

@ -35,4 +35,68 @@ Send</button></p>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<!--<script src="/assets/scripts/openpgp.min.js" integrity="sha512-xAGrSbnTUtvQAlyLlVQr6MM7yltwOQ8NEjDhDorsB4u8ANG7bRxXLU3fVaHlAi8/9r9et1Is4s6/Aaw56g+a8A=="></script>-->
<script src="/assets/scripts/contact-form.js" integrity="sha512-USdJjNf5Va8fIYW6KBqkBaUlnKaaPU12OUEe41d90ApB8GYlJDs1gEPBHGYFJCLNUgdmdP/0bHO9eGybm+Z/2w=="></script>
<script>/* global openpgp fetch hcaptcha ko */
const API_URL = 'https://mailapi.slvit.us/'
function Form() {
let self = this
self.name = ko.observable('')
self.email = ko.observable('')
self.subj = ko.observable('')
self.body = ko.observable('')
self.isSending = ko.observable(false)
self.sendBtnText = ko.computed(() => self.isSending()?'Sending...':'Send')
self.sendMsg = async () => {
self.isSending(true)
let capRes; try {
capRes = await hcaptcha.execute(
null, {async:true}
)
}
catch (err) {
alert(`Failed to submit hCaptcha. Try again later.`)
console.error('Failed to run hCaptcha')
if (err) console.error(err)
}
let res; try {
res = await fetch(API_URL, {
method: 'POST',
cache: 'no-cache',
headers: {'content-type': 'application/json'},
body: JSON.stringify({
token: capRes.response,
name: self.name(),
subj: self.subj(),
email: self.email(),
msg: self.body(),
// msg: await openpgp.encrypt({
// message: await openpgp.createMessage(
// { text: `\n${self.body()}\n\n` }
// ),
// encryptionKeys: await openpgp.readKey({
// armoredKey: `-----BEGIN PGP PUBLIC KEY BLOCK-----
// =Wvbt
// -----END PGP PUBLIC KEY BLOCK-----`,
// }),
// }),
}),
})
} catch (err) {
console.error(err)
alert(`Failed to connect to the network. Are you online?`)
} finally { self.isSending(false) }
if (res.status===200) {
alert('Your message was sent successfully.')
self.name(''); self.email(''); self.subj(''); self.body('')
} else if (res.status===403)
alert(`hCaptcha failed! Please try again.`)
else if (res.status===500)
alert(`Backend failed! Please try again. If the problem persists, please email hostmaster@[this domain].`)
else alert(`Unknown error! Please try again. If the problem persists, please email hostmaster@[this domain].`)
}
} ko.applyBindings(new Form())
</script>

View File

@ -20,9 +20,9 @@
<header>
<div id="topbar">
<aside>
<a href="https://speedtest.slvit.us/">Speedtest</a> | <a href="https://searx.slvit.us/">Web search</a>
<a href="/speedtest/">Speedtest</a> | <a href="/websearch/">Web search</a>
</aside>
<aside>
<aside id="rh-aside">
<a href="/login">Login</a>
</aside>
</div>
@ -119,4 +119,11 @@
Modified: {{page.date}}
-->
</body>
<script>
let rhAside = document.getElementById('rh-aside')
if (window.localStorage.user_id)
rhAside.innerHTML = (window.localStorage.user_isAdmin==='true')?
'<a href="/admin">Admin</a> | <a href="/dashboard">Dashboard</a> | <a href="/logout">Logout</a>'
: '<a href="/dashboard">Dashboard</a> | <a href="/logout">Logout</a>'
</script>
</html>

View File

@ -0,0 +1,109 @@
<noscript><p>Uh-oh, you don't have javascript. This you can't log in without it! </p></noscript>
<p><input
type="text"
data-bind="textInput:username, event:{keypress:inputKeypress}"
placeholder="Username"
required>
</p>
<p><input
type="password"
data-bind="textInput:password, event:{keypress:inputKeypress}"
placeholder="Password"
required>
</p>
<p><button id="submit-button"
data-bind="click:submitLogin, disable:isLoggingIn, text:submitBtnText">
</button></p>
{# <p><div class="h-captcha"
data-sitekey="XXXXXXXXXXXXXXX"
data-size="invisible"
data-theme="dark">
</div></p> #}
{# <p>This form is protected by <a href="https://www.hcaptcha.com/">hCaptcha</a> so their <a href="https://hcaptcha.com/privacy">Privacy Policy</a> and <a href="https://hcaptcha.com/terms">Terms of Service</a> apply.</p> #}
{# <script src="https://js.hcaptcha.com/1/api.js" async defer></script> #}
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script>/* global ko fetch */
//const API_URL = 'https://api.slvit.us'
const API_URL = 'https://dev-api.ksx.ki9.slvit'
// Redirect if already logged in
if (window.localStorage.user_id==='true') window.location.href = '/dashboard'
function Form() {
let self = this
self.username = ko.observable('')
self.password = ko.observable('')
self.isLoggingIn = ko.observable(false)
self.submitBtnText = ko.computed(() =>
self.isLoggingIn()?'Logging in...':'Log in')
// Submit form on pressing ENTER
// https://stackoverflow.com/a/25055138/3006854
self.inputKeypress = (d,e) => {
e.keyCode === 13 && self.submitLogin()
return true
}
self.reset = () => {
self.isLoggingIn(false)
self.username('')
self.password('')
}
self.fail = (err) => {
if (err) console.error(err)
alert('Log in failed!')
}
self.submitLogin = async () => {
self.isLoggingIn(true)
//TODO: Validate the form first!
// Check captcha
//let capRes; try {
// capRes = await hcaptcha.execute(
// null, {async:true}
// )
//} catch (err) {
// alert(`Failed to submit hCaptcha. Try again later.`)
// console.error('Failed to run hCaptcha')
// if (err) console.error(err)
//}
// Send login data
try {
const res = await fetch(`${API_URL}/login`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type':'application/json' },
body: JSON.stringify({
username: self.username(),
password: self.password(),
})
})
if (res.ok) {
try {
const user = await res.json()
window.localStorage.user_id = user._id
window.localStorage.user_email = user.email
window.localStorage.user_isAdmin = user.isAdmin
window.localStorage.user_firstName = user.name.first
} catch (err) {
console.error('Failed to parse response!')
if (err) console.error(err)
}
window.location.href = '/dashboard'
}
else alert('Login failed')
}
catch (err) { self.fail(err) }
finally { self.reset() }
}
} ko.applyBindings(new Form())
</script>

View File

@ -0,0 +1,15 @@
<noscript><p>Hey! This search won't work witout javascript. You can still perform a search from <a href="https://searxng.slvit.us/">searxng.slvit.us</a> directly.</p></noscript>
<div style="display:flex;justify-content:space-around;margin:5vh auto">
<input id="search-box" type="text" placeholder="Enter your query here" style="width:80%">
<button id="search-btn" onClick="search()" style="width:fit-content">Search</button>
</div>
<script>
const searchBox = document.getElementById('search-box')
const search = () => window.location.href =
`https://searxng.slvit.us/?q=${encodeURIComponent(searchBox.value)}`
searchBox.addEventListener('keyup', (e) => {
if (!e) e = window.event
if (e.keyCode === 13) search()
else document.getElementById('search-btn').disabled = (searchBox.value.length===0)
}, false)
</script>

11
_src/admin/index.md Normal file
View File

@ -0,0 +1,11 @@
---
layout: layouts/base.njk
---
# Admin
- [Users](users)
- [Invoices](invoices)
- [Instances](instances)
- [Items](items)
- [Offerings](offerings)

View File

@ -0,0 +1,172 @@
---
layout: layouts/base.njk
---
<h1>Edit Instance</h1>
<noscript><p>Uh-oh, you don't have javascript. You can't edit this instance without it! </p></noscript>
<a href="/admin/users/">&lt; Back to instance list</a>
<p><input placeholder="Name"
type="text"
data-bind="textInput:name"
required>
</p>
<p><input
type="text"
data-bind="textInput:id"
required disabled>
</p>
<p style="display:flex;justify-content:space-around">
<span>Offering: </span>
<select style="min-width:80%"
data-bind="options:availableOfferings, value:selectedOffering">
</select>
</p>
<p><input placeholder="User ID"
type="text"
data-bind="textInput:user"
required>
</p>
<p style="display:flex;justify-content:space-bewtween">
<span>Rate:</span>
<input
type="number"
data-bind="textInput:rate"
min="0"
step="0.01"
placeholder="Enter an amount"
required>
<span>USD/</span>
<select data-bind="options:availablePeriods, value:selectedPeriod"></select>
</p>
<p><textarea
type="text"
data-bind="textInput:note"
placeholder="Note (optional, not visible to customer)"></textarea>
</p>
<p><button
data-bind="click:updateInstance, disable:isUpdating, text:saveBtnText">
</button></p>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global URLSearchParams fetch ko */
//const API_URL = 'https://api.slvit.us/admin/instances'
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin/instances'
function Page() {
let self = this
self.name = ko.observable('')
self.id = ko.observable('')
self.note = ko.observable('')
self.user = ko.observable('')
self.note = ko.observable('')
self.period = ko.observable('')
self.rate = ko.observable(0)
self.availableOfferings = ko.observableArray([])
self.selectedOffering = ko.observable('')
self.availablePeriods = ko.observableArray([
'onetime',
'hourly',
'daily',
'weekly',
'monthly',
'quarterly',
'yearly',
])
self.selectedPeriod = ko.observable('')
self.isUpdating = ko.observable(false)
self.saveBtnText = ko.computed(() =>
self.isUpdating()?'Saving...':'Save')
// Get query params
const urlParams = new URLSearchParams(window.location.search)
const instanceid = urlParams.get('id')
self.updateInstance = async () => {
self.isUpdating(true)
//TODO: Validate inputs, including API requests
try {
const res = await fetch(`${API_URL}/${instanceid}/`, {
method: 'PUT',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: self.name(),
note: self.note(),
offering: self.selectedOffering(),
user: self.user(),
rate: self.rate(),
period: self.selectedPeriod(),
}),
})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
}
else if (res.status===404)
alert('No instances found!')
else if (res.status===502)
alert('Proxy error; server is down or restarting...')
else if (res.status===500)
alert('Server error!')
else alert(`${res.status} error received!`)
} else window.location.href = '/admin/instances/'
} catch (err) {
if (err) console.error(err)
alert('Update request failed')
} finally { self.isUpdating(false) }
}
// Load instance
;(async () => {
try {
const res = await fetch(`${API_URL}/${instanceid}/`,
{credentials:'include'})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
} else if (res.status===404)
alert('User or instance not found!')
else if (res.status===502)
alert('Proxy error; server is down or restarting...')
else if (res.status===500)
alert('Server error!')
else alert(`${res.status} error received!`)
return window.location.href = '/admin/instances/'
} else try {
const data = await res.json()
console.log(data)
self.id(data.instance._id)
self.name(data.instance.name)
self.note(data.instance.note)
self.availableOfferings(data.offerings)
self.user(data.instance.user.username)
self.rate(data.instance.rate)
self.period(data.instance.period)
self.selectedOffering(data.instance.offering.name)
} catch(err) {
console.error('Failed to parse response!')
if(err) console.error(err)
alert(`Failed to load data!`)
}
} catch (err) {
console.error('Failed to send data request!')
if (err) console.error(err)
alert(`Failed to load data! Are you online?`)
}
})()
} ko.applyBindings(new Page())
</script>

View File

@ -0,0 +1,120 @@
---
layout: layouts/base.njk
title: Instances
---
<h1>{{title}}</h1>
<noscript><p>This dashboard won't work without javascript!</p></noscript>
<div class="flex">
<a href="/admin/">&lt; Back to admin dashboard</a>
<button onclick="window.location.href='new'">Create new instance</button>
</div>
<table>
<thead><tr>
<th>Name</th>
<th>Offering</th>
<th>User</th>
<th>Rate</th>
<th>Actions</th>
</tr></thead>
<tbody data-bind="foreach:instances"><tr>
<td data-bind="text:name"></td>
<td data-bind="text:offering"></td>
<td data-bind="text:user"></td>
<td>
$<span data-bind="text:rate"></span>
<span data-bind="text:period"></span>
</td>
<td>
<button data-bind="click:$parent.editInstance">Edit</button>
<button data-bind="click:$parent.delInstance, disable:$data.isDeleting, text:$data.deleteText">Delete</button>
</td>
</tr></tbody>
</table>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global fetch ko */
//const API_URL = 'https://api.slvit.us/admin'
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin'
function Instance(data) {
this.id = ko.observable(data._id)
this.name = ko.observable(data.name)
this.offering = ko.observable(data.offering.name)
this.user = ko.observable(data.user)
this.rate = ko.observable(data.rate)
this.period = ko.observable(data.period)
this.isDeleting = ko.observable(false)
this.deleteText = ko.computed(() =>
this.isDeleting()?'Deleting...':'Delete')
}
function Page() {
let self = this
self.instances = ko.observableArray([])
// Delete instance
self.delInstance = async (instance) => {
instance.isDeleting(true)
try {
const res = await fetch(`${API_URL}/instances/${instance.id()}`, {
method: 'DELETE',
credentials: 'include',
})
if (!res.ok) {
alert(`Request to delete ${instance.name()} failed!`)
console.log(res)
} else {
self.instances.remove(instance)
}
} catch (err) {
alert(`Failed to send request to delete ${instance.name()}`)
if (err) console.error(err)
} finally {
instance.isDeleting(false)
}
}
// Edit instance
self.editInstance = (instance) => {
console.log(`editInstance called for ${JSON.stringify(instance)}`)
window.location.href = `edit/?id=${instance.id()}`
}
// Load data
;(async() => {
try {
const res = await fetch(`${API_URL}/instances/`, {credentials:'include'})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
}
else if (res.status===404) alert('No instances found!')
else if (res.status===502) alert('Proxy error; server is down or restarting...')
else if (res.status===500) alert('Server error!')
else alert(`${res.status} error received!`)
} else try {
self.instances( (await res.json())
.map((instance) => new Instance(instance) )
)
} catch(err) {
console.error('Failed to parse response!')
if(err) console.error(err)
alert(`Failed to load data!`)
}
} catch (err) {
console.error('Failed to send data request!')
if (err) console.error(err)
alert(`Failed to load data! Are you online?`)
}
})()
} ko.applyBindings(new Page())
</script>

View File

@ -0,0 +1,161 @@
---
layout: layouts/base.njk
---
<h1>New Instance</h1>
<noscript><p>Uh-oh, you don't have javascript. You can't create an instance without it! </p></noscript>
<a href="/admin/instances/">&lt; Back to instance list</a>
<p><input placeholder="Name"
type="text"
data-bind="textInput:name"
required>
</p>
<p style="display:flex;justify-content:space-around">
<span>Offering: </span>
<select style="min-width:80%"
data-bind="options:availableOfferings, optionsText:(o)=>Object.values(o)[0], optionsValue:(o)=>Object.keys(o)[0], value:$root.selectedOffering">
</select>
</p>
<p><input placeholder="User ID"
type="text"
data-bind="textInput:user,disable:userFieldDisabled"
required>
</p>
<p style="display:flex;justify-content:space-bewtween">
<span>Rate:</span>
<input
type="number"
data-bind="textInput:rate"
min="0"
step="0.01"
placeholder="Enter an amount"
required>
<span>USD/</span>
<select data-bind="options:availablePeriods, value:selectedPeriod"></select>
</p>
<p><textarea
type="text"
data-bind="textInput:note"
placeholder="Note (optional, not visible to customer)"></textarea>
</p>
<p><button
data-bind="click:createInstance, disable:isCreating, text:addBtnText">
</button></p>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global fetch ko URLSearchParams */
//const API_URL = 'https://api.slvit.us/admin/instances/'
const API_URL = 'https://dev-api.ksx.ki9.slvit'
// Get query params
const urlParams = new URLSearchParams(window.location.search)
const userid = urlParams.get('id')
function Page() {
let self = this
self.name = ko.observable('')
self.user = ko.observable(userid||'')
self.note = ko.observable('')
self.rate = ko.observable(0)
self.availableOfferings = ko.observableArray([])
self.selectedOffering = ko.observable('')
self.availablePeriods = ko.observableArray([
'onetime',
'hourly',
'daily',
'weekly',
'monthly',
'quarterly',
'yearly',
])
self.selectedPeriod = ko.observable('')
self.isCreating = ko.observable(false)
self.addBtnText = ko.computed(() =>
self.isCreating()?'Adding...':'Add')
self.userFieldDisabled = ko.computed( () => (userid) )
self.createInstance = async () => {
self.isCreating(true)
//TODO: Validate inputs, including API requests
try {
const res = await fetch(`${API_URL}/admin/instances/`, {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: self.name(),
note: self.note(),
user: self.user(),
rate: self.rate(),
period: self.selectedPeriod(),
offering: self.selectedOffering(),
}),
})
if (!res.ok) {
if (res.status===403) {
window.localStorage.clear()
window.location.href = '/login/'
} else {
console.error(res)
try {
alert(await res.text())
} catch (err) {
if (err) console.error(err)
alert(`Error response`)
}
}
} else window.location.href = '/admin/instances/'
} catch (err) {
if (err) console.error(err)
alert('Create request failed')
} finally { self.isCreating(false) }
}
// Load possible offerings
;(async () => {
try {
const res = await fetch(`${API_URL}/admin/offerings?short=true`,
{credentials:'include'})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
} else if (res.status===404) alert('Instance not found!')
else if (res.status===502) alert('Proxy error; server is down or restarting. Try again later or contact support if it stays down.')
else if (res.status===500) alert('Server error!')
else alert(`${res.status} error received!`)
return window.location.href = '/admin/instances/'
} else try {
const parsedRes = await res.json()
try {
const mappedRes = parsedRes.map((o) => JSON.parse(o))
console.log(mappedRes)
self.availableOfferings(mappedRes)
} catch (err) {
console.error('Failed to parse objects!')
if (err) console.error(err)
}
} catch (err) {
console.error('Failed to parse response!')
if(err) console.error(err)
alert(`Failed to load data!`)
}
} catch (err) {
console.error('Failed to send data request!')
if (err) console.error(err)
alert(`Failed to load data! Are you online?`)
}
})()
} ko.applyBindings(new Page())
</script>

View File

@ -0,0 +1,158 @@
---
layout: layouts/base.njk
---
<h1>Edit Instance</h1>
<noscript><p>Uh-oh, you don't have javascript. You can't edit this instance without it! </p></noscript>
<a href="/admin/users/">&lt; Back to instance list</a>
<p><input placeholder="Name"
type="text"
data-bind="textInput:name"
required>
</p>
<p style="display:flex;justify-content:space-around">
<span>Offering: </span>
<select style="min-width:80%"
data-bind="options:availableOfferings, value:selectedOffering">
</select>
</p>
<p><input placeholder="User ID"
type="text"
data-bind="textInput:user"
required>
</p>
<p style="display:flex;justify-content:space-bewtween">
<span>Rate:</span>
<input
type="number"
data-bind="textInput:rate"
min="0"
step="0.01"
placeholder="Enter an amount"
required>
<span>USD/</span>
<select data-bind="options:availablePeriods, value:selectedPeriod"></select>
</p>
<p><textarea
type="text"
data-bind="textInput:note"
placeholder="Note (optional, not visible to customer)"></textarea>
</p>
<p><button
data-bind="click:updateInstance, disable:isUpdating, text:saveBtnText">
</button></p>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global URLSearchParams fetch ko */
//const API_URL = 'https://api.slvit.us/admin/instances'
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin/instances'
function Page() {
let self = this
self.name = ko.observable('')
self.note = ko.observable('')
self.user = ko.observable('')
self.note = ko.observable('')
self.period = ko.observable('')
self.rate = ko.observable(0)
self.availableOfferings = ko.observableArray([])
self.selectedOffering = ko.observable('')
self.availablePeriods = ko.observableArray([
'onetime',
'hourly',
'daily',
'weekly',
'monthly',
'quarterly',
'yearly',
])
self.selectedPeriod = ko.observable('')
self.isUpdating = ko.observable(false)
self.saveBtnText = ko.computed(() =>
self.isUpdating()?'Saving...':'Save')
// Get query params
const urlParams = new URLSearchParams(window.location.search)
const instanceid = urlParams.get('id')
self.updateInstance = async () => {
self.isUpdating(true)
//TODO: Validate inputs, including API requests
try {
const res = await fetch(`${API_URL}/${instanceid}/`, {
method: 'PUT',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: self.name(),
note: self.note(),
offering: self.selectedOffering(),
user: self.user(),
rate: self.rate(),
period: self.selectedPeriod(),
}),
})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
}
else if (res.status===404) alert('No instances found!')
else if (res.status===502) alert('Proxy error; server is down or restarting...')
else if (res.status===500) alert('Server error!')
else alert(`${res.status} error received!`)
} else window.location.href = '/admin/instances/'
} catch (err) {
if (err) console.error(err)
alert('Update request failed')
} finally { self.isUpdating(false) }
}
// Load instance
;(async () => {
try {
const res = await fetch(`${API_URL}/${instanceid}/`,
{credentials:'include'})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
} else if (res.status===404) alert('User or instance not found!')
else if (res.status===502) alert('Proxy error; server is down or restarting...')
else if (res.status===500) alert('Server error!')
else alert(`${res.status} error received!`)
return window.location.href = '/admin/instances/'
} else try {
const data = await res.json()
console.log(data)
self.name(data.instance.name)
self.note(data.instance.note)
self.availableOfferings(data.offerings)
self.user(data.instance.user.username)
self.rate(data.instance.rate)
self.period(data.instance.period)
self.selectedOffering(data.instance.offering.name)
} catch(err) {
console.error('Failed to parse response!')
if(err) console.error(err)
alert(`Failed to load data!`)
}
} catch (err) {
console.error('Failed to send data request!')
if (err) console.error(err)
alert(`Failed to load data! Are you online?`)
}
})()
} ko.applyBindings(new Page())
</script>

View File

@ -0,0 +1,109 @@
---
layout: layouts/base.njk
title: Invoices
---
<h1>{{title}}</h1>
<noscript><p>This dashboard won't work without javascript!</p></noscript>
<div class="flex">
<a href="/admin/">&lt; Back to admin dashboard</a>
<button onclick="window.location.href='new'">Create new invoice</button>
</div>
<p data-bind="visible:invoices().length===0">No invoices!</p>
<table data-bind="visible:invoices().length!==0">
<thead><tr>
<th>ID</th>
<th>Actions</th>
</tr></thead>
<tbody data-bind="foreach:invoices"><tr>
<td data-bind="text:id"></td>
<td>
<button data-bind="click:$parent.editInvoice">Edit</button>
<button data-bind="click:$parent.delInvoice, disable:$data.isDeleting, text:$data.deleteText">Delete</button>
</td>
</tr></tbody>
</table>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global fetch ko */
//const API_URL = 'https://api.slvit.us/admin'
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin'
function Invoice(data) {
this.id = ko.observable(data._id)
this.name = ko.observable(data.name)
this.user = ko.observable(data.user)
this.isDeleting = ko.observable(false)
this.deleteText = ko.computed(() =>
this.isDeleting()?'Deleting...':'Delete')
}
function Page() {
let self = this
self.invoices = ko.observableArray([])
// Delete invoice
self.delInvoice = async (invoice) => {
invoice.isDeleting(true)
try {
const res = await fetch(`${API_URL}/invoices/${invoice.id()}`, {
method: 'DELETE',
credentials: 'include',
})
if (!res.ok) {
alert(`Request to delete ${invoice.name()} failed!`)
console.log(res)
} else {
self.invoices.remove(invoice)
}
} catch (err) {
alert(`Failed to send request to delete ${invoice.name()}`)
if (err) console.error(err)
} finally {
invoice.isDeleting(false)
}
}
// Edit invoice
self.editInvoice = (invoice) => {
console.log(`editInvoice called for ${JSON.stringify(invoice)}`)
window.location.href = `edit/?id=${invoice.id()}`
}
// Load data
;(async() => {
try {
const res = await fetch(`${API_URL}/invoices/`, {credentials:'include'})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
}
else if (res.status===404) alert('No invoices found!')
else if (res.status===502) alert('Proxy error; server is down or restarting...')
else if (res.status===500) alert('Server error!')
else alert(`${res.status} error received!`)
} else try {
self.invoices( (await res.json())
.map((invoice) => new Invoice(invoice) )
)
} catch(err) {
console.error('Failed to parse response!')
if(err) console.error(err)
alert(`Failed to load data!`)
}
} catch (err) {
console.error('Failed to send data request!')
if (err) console.error(err)
alert(`Failed to load data! Are you online?`)
}
})()
} ko.applyBindings(new Page())
</script>

573
_src/admin/invoices/new.njk Normal file
View File

@ -0,0 +1,573 @@
---
layout: layouts/base.njk
---
<h1>New Invoice</h1>
<noscript><p>Uh-oh, you don't have javascript. You can't create an invoice without it! </p></noscript>
<a href="/admin/invoices/">&lt; Back to invoice list</a>
<p style="display:flex;justify-content:space-between">
<span>User: </span>
<input placeholder="Username"
type="text"
data-bind="textInput:username, disable:userFieldDisabled, hasFocus:userFieldFocused"
required>
</p>
<p style="display:flex;justify-content:space-between">
<span>Date ordered</span>
<input type="date"
data-bind="value:date.ordered"
required>
</p>
<p><textarea
type="text"
data-bind="textInput:note"
placeholder="Note (optional, not visible to customer)"></textarea>
</p>
<hr>
<h2>Instances</h2>
<p data-bind="visible:instances().length===0">No instances</p>
<table data-bind="visible:instances().length!==0">
<thead><tr>
<th>Name</th>
<th>Period</th>
<th>Price</th>
<th>Action</th>
</tr></thead>
<tbody data-bind="foreach:instances"><tr>
<td data-bind="text:name"></td>
<td><span data-bind="text:period.start"></span> to <span data-bind="text:period.end"></span></td>
<td data-bind="text:priceString"></td>
<td>
<button data-bind="text:delTxt, click:$parent.delInstance, disable:$data.isDeleting"></button>
</td>
</tr></tbody>
</table>
<h3>Add instance</h3>
<p data-bind="visible:!userValid()">Enter a valid user to add instances</p>
<p data-bind="visible:userValid()&&availableInstances().length===0">User <b data-bind="text:username"></b> has no instances.</p>
<table data-bind="visible:userValid()&&availableInstances().length!==0">
<thead><tr>
<th>Instance</th>
<th>Price</th>
<th>Start</th>
<th>End</th>
</tr></thead>
<tbody><tr>
<td><select data-bind="options:availableInstances, optionsText:'name', optionsValue:'_id', value:$root.selectedInstance, event:{change:addInstanceChanged}" required></select>
</td>
<td>
$<span data-bind="text:selectedInstanceRate"></span> <span data-bind="text:selectedInstancePeriod"></span>
</td>
<td><input type="date" required
data-bind="textInput:addInstanceStart">
</td>
<td data-bind="text:addInstanceEnd">
</td>
<td><button data-bind="text:addInstanceTxt, click:addInstance, disabled:isAddingInstance"></button>
</td>
</tr></tbody>
</table>
<hr>
<h2>Items</h2>
<p data-bind="visible:items().length===0">No items</p>
<table data-bind="visible:items().length!==0">
<thead><tr>
<th>Qty</th>
<th>Item</th>
<th>Subtotal</th>
<th>Tax</th>
<th>Total</th>
<th>Action</th>
</tr></thead>
<tbody data-bind="foreach:items"><tr>
<td data-bind="text:qty"></td>
<td data-bind="text:name"></td>
<td data-bind="text:'$'+subtotal.toFixed(2)"></td>
<td data-bind="text:'$'+tax.toFixed(2)"></td>
<td data-bind="text:'$'+total.toFixed(2)"></td>
<td>
<button data-bind="text:delTxt, click:$parent.delItem, disable:$data.isDeleting"></button>
</td>
</tr></tbody>
</table>
<h3>Add item</h3>
<table>
<thead><tr>
<th>ID</th>
<th>Name</th>
<th>Price (USD)</th>
<th>Qty</th>
<th>Subtotal</th>
<th>Tax</th>
<th>Total</th>
</tr></thead>
<tbody><tr>
<td>
<input type="text"
data-bind="textInput:newItemOid, hasFocus:newItemFocused"
required></select>
</td><td data-bind="text:newItemName">
</td><td>
<input type="number"
data-bind="textInput:newItemPrice, disabled:"
step="0.01" min="0"
required>
</td><td>
<input type="number" min="0"
data-bind="textInput:newItemQty"
required>
</td>
<td>$<span data-bind="text:newItemSubtotal().toFixed(2)"></span></td>
<td>$<span data-bind="text:newItemTax().toFixed(2)"></span></td>
<td>$<span data-bind="text:newItemTotal().toFixed(2)"></span></td>
</tr></tbody>
</table>
<button data-bind="text:addItemTxt, click:addItem, disabled:isAddingItem"></button>
<hr>
<h2>Shipment</h2>
<p data-bind="visible:items().length===0">No items to ship</p>
<div data-bind="visible:items().length!==0">
<p style="display:flex;justify-content:space-between">
<span>Date shipped</span>
<input type="date"
data-bind="value:date.shipped">
</p>
<p style="display:flex;justify-content:space-between">
<span>Date delivered</span>
<input type="date"
data-bind="value:date.delivered">
</p>
<p style="display:flex;justify-content:space-between">
<span>Carrier</span>
<input type="text"
data-bind="value:carrier">
</p>
<p style="display:flex;justify-content:space-between">
<span>Tracking number</span>
<input type="text"
data-bind="value:trackingNumber">
</p>
<p style="display:flex;justify-content:space-between">
<span>Cost</span>
<input type="number"
min="0" step=".01"
data-bind="value:shippingFee">
</p>
</div>
<hr>
<h2>Payment</h2>
<p style="display:flex;justify-content:space-between">
<span>Date due</span>
<input type="date"
data-bind="value:date.due">
</p>
<p style="display:flex;justify-content:space-between">
<span>Date paid</span>
<input type="date"
data-bind="value:date.paid">
</p>
<p style="display:flex;justify-content:space-between">
<span>Adjustments</span>
<input type="Number"
step="0.01"
data-bind="value:adjustments">
</p>
<p style="display:flex;justify-content:space-between">
<span>Subtotal</span>
<span data-bind="text:subtotalString()"></span>
</p>
<p style="display:flex;justify-content:space-between">
<span>Tax</span>
<span data-bind="text:taxString()"></span>
</p>
<p style="display:flex;justify-content:space-between"
data-bind="style: { display: items().length!==0 ? 'flex' : 'none' }">
<span>Shipping</span>
<span data-bind="text:shippingFeeString()"></span>
</p>
<p style="display:flex;justify-content:space-between"
data-bind="style: { display: adjustments()!==0 ? 'flex' : 'none' }">
<span data-bind="text:adjustmentLabel"></span>
<span data-bind="text:adjustmentsString"></span>
</p>
<p style="display:flex;justify-content:space-between">
<span>Total</span>
<span data-bind="text:totalString"></span>
</p>
<hr>
<p><button
data-bind="click:createInvoice, disable:isCreating, text:addBtnText">
</button></p>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global fetch ko URLSearchParams */
//const API_URL = 'https://api.slvit.us/admin'
const API_URL = 'https://dev-api.ksx.ki9.slvit'
const SALES_TAX = 0.049
const urlParams = new URLSearchParams(window.location.search)
// Sum an array
// https://stackoverflow.com/a/16751601/3006854
const sum = i => i.reduce((s,a)=>s+a, 0)
function Instance(data) {
this.name = data.name
this.id = data.id
this.price = data.price
this.priceString = '$' + this.price.toFixed(2)
this.period = data.period
this.isDeleting = ko.observable(false)
this.tax = 0 // No tax for services in Colorado
this.delTxt = ko.pureComputed( () =>
this.isDeleting()?'Deleting...':'Delete' )
}
function Item(data) {
this.id = data._id
this.name = data.name
this.qty = Number(data.qty)
this.price = data.price
this.subtotal = data.subtotal
this.tax = data.tax
this.total = data.total
this.isDeleting = ko.observable(false)
this.delTxt = ko.pureComputed( () =>
this.isDeleting()?'Deleting...':'Delete')
}
function Page() {
let self = this
self.username = ko.observable(urlParams.get('user')||'')
self.userid = ko.observable('')
self.userFieldFocused = ko.observable(false)
self.userValid = ko.observable(false)
let oldusername
self.userFieldFocused.subscribe(async (f) => {
if (f) oldusername = self.username()
else {
if (oldusername!==self.username()&&self.username()!=='') {
try {
const res = await fetch(`${API_URL}/admin/users/un/${self.username()}`,
{credentials:'include'}
)
if (res.ok) {
self.userValid(true)
self.instances([])
try {
const parsed = await res.json()
console.log(parsed)
self.availableInstances(parsed.instances)
self.userid(parsed._id)
} catch (err) {
console.error(res)
console.error(err)
alert('Failed to parse json!')
}
} else {
self.userValid(false)
if (res.status===403) logout()
else if (res.status===404)
alert('User not found!')
else if (res.status===502)
alert('Proxy error; server is down or restarting...')
else if (res.status===500)
alert('Server error!')
else {
alert(`${res.status} error received!`)
console.error(res)
}
}
} catch (err) {
if (err) console.error(err)
alert('Failed to request user info from server!')
}
}
}
})
self.userFieldDisabled = ko.pureComputed( () => (urlParams.get('user')) )
self.note = ko.observable('')
self.date = {
ordered: ko.observable(''),
due: ko.observable(''),
paid: ko.observable(''),
shipped: ko.observable(''),
delivered: ko.observable(''),
}
// Items
self.items = ko.observableArray([])
self.delItem = async (item) =>
self.items.remove(item)
self.addItem = async () => {
self.isAddingItem(true)
self.items.push( new Item({
name: self.newItemName(),
_id: self.newItemOid(),
qty: self.newItemQty(),
price: self.newItemPrice(),
subtotal: self.newItemSubtotal(),
tax: self.newItemTax(),
total: self.newItemTotal(),
}) )
// Reset form
self.newItemName('')
self.newItemOid('')
self.newItemQty(1)
self.newItemPrice(0)
self.isAddingItem(false)
}
self.isAddingItem = ko.observable(false)
self.addItemTxt = ko.pureComputed(() =>
self.isAddingItem()?'Adding...':'Add item')
self.newItemFocused = ko.observable(false)
self.newItemName = ko.observable('')
let oldNewItemOid
// Fetch item details on change
self.newItemFocused.subscribe(async (f) => {
if (f) oldNewItemOid = self.newItemOid()
else {
if (oldNewItemOid!==self.newItemOid()) {
try {
const res = await fetch(
`${API_URL}/admin/items/${self.newItemOid()}`,
{credentials:'include'},
)
if (res.ok) {
try {
const parsed = await res.json()
console.log(parsed)
self.newItemPrice(parsed.price)
self.newItemName(parsed.name)
} catch (err) {
console.error(res)
console.error(err)
alert('Failed to parse json!')
}
} else {
if (res.status===403) logout()
else if (res.status===400)
alert('Bad item ID format!')
else if (res.status===404)
alert('Item not found!')
else if (res.status===502)
alert('Proxy error; server is down or restarting...')
else if (res.status===500)
alert('Server error!')
else {
alert(`${res.status} error received!`)
console.error(res)
}
}
} catch (err) {
if (err) console.error(err)
alert(`Failed to request item info from server!`)
}
}
}
})
self.newItemOid = ko.observable('')
self.newItemPrice = ko.observable(0)
self.newItemQty = ko.observable(1)
self.newItemSubtotal = ko.pureComputed( () =>
Math.round((
(self.newItemQty() * self.newItemPrice())
+ Number.EPSILON) *100
) / 100 )
self.newItemTax = ko.pureComputed( () =>
Math.round((
(self.newItemSubtotal() * SALES_TAX)
+ Number.EPSILON) * 100
) / 100 )
self.newItemTotal = ko.pureComputed( () =>
self.newItemSubtotal() + self.newItemTax() )
// Instances
self.instances = ko.observableArray([])
self.availableInstances = ko.observableArray([])
self.selectedInstance = ko.observable('')
self.selectedInstanceObj = ko.observable({})
self.selectedInstanceRate = ko.pureComputed( () =>
self.selectedInstanceObj().rate )
self.selectedInstancePeriod = ko.computed( () =>
self.selectedInstanceObj().period )
self.addInstanceChanged = async (context) => {
self.selectedInstanceObj(self.availableInstances().filter(
(i)=>i._id===context.selectedInstance())[0]
) }
self.addInstanceOid = ko.observable('')
self.addInstanceStart = ko.observable('')
self.addInstanceEnd = ko.computed( () => {
// Just using dates right now but can use datetimes later
// https://stackoverflow.com/a/31732581/3006854
if (self.addInstanceStart()==='') return ''
if (self.selectedInstanceObj().period==='onetime') {
return new Date(self.addInstanceStart())
} else if (self.selectedInstanceObj().period==='hourly') {
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
enddate.setHours(enddate.getHours() + 1)
return enddate.toISOString().slice(0,10)
} else if (self.selectedInstanceObj().period==='daily') {
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
enddate.setDate(enddate.getDate() + 1)
return enddate.toISOString().slice(0,10)
} else if (self.selectedInstanceObj().period==='weekly') {
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
enddate.setDate(enddate.getDate() + 7)
return enddate.toISOString().slice(0,10)
} else if (self.selectedInstanceObj().period==='monthly') {
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
enddate.setMonth(enddate.getMonth() + 1)
return enddate.toISOString().slice(0,10)
} else if (self.selectedInstanceObj().period==='quarterly') {
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
enddate.setMonth(enddate.getMonth() + 3)
return enddate.toISOString().slice(0,10)
} else if (self.selectedInstanceObj().period==='yearly') {
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
enddate.setFullYear(enddate.getFullYear() + 1)
return enddate.toISOString().slice(0,10)
} else return null
})
self.isAddingInstance = ko.observable(false)
self.addInstance = async () => {
self.isAddingInstance(true)
let instance = self.availableInstances()
.filter((i) => i._id===self.selectedInstance())[0]
console.log(`Adding instance: ${instance.name}`)
self.instances.push( new Instance({
id: instance._id,
name: instance.name,
period: {
start: self.addInstanceStart(),
end: self.addInstanceEnd(),
},
price: instance.rate,
}) )
self.isAddingInstance(false)
self.selectedInstance('')
self.addInstanceStart('')
}
self.addInstanceTxt = ko.pureComputed(() =>
self.isAddingInstance()?'Adding...':'Add instance')
self.delInstance = async (instance) =>
self.instances.remove(instance)
// Payment/shipment
self.shippingFee = ko.observable(0)
self.shippingFeeInt = ko.computed( () =>
Number(self.shippingFee()))
self.shippingFeeString = ko.pureComputed( () =>
'$' + self.shippingFeeInt().toFixed(2))
self.subtotal = ko.computed( () => {
if (self.items().length + self.instances().length === 0)
return 0
else if (self.items().length===0)
return sum(self.instances().map((i)=>i.price))
else if (self.instances().length===0) {
return sum(self.items().map(i=>i.subtotal))
}
else return ( sum(self.items().map(i=>i.subtotal)) +
sum(self.instances().map(i=>i.price)) )
} )
self.subtotalString = ko.pureComputed( () =>
'$' + self.subtotal().toFixed(2) )
self.tax = ko.computed( () => {
if (self.items().length===0) return 0
else return sum(self.items().map(i=>i.tax))
} )
self.taxString = ko.pureComputed( () =>
'$' + self.tax().toFixed(2) )
self.adjustments = ko.observable(0)
self.adjustmentsInt = ko.computed( () =>
Number(self.adjustments()) )
self.adjustmentsVisible = ko.pureComputed(
() => (self.adjustmentsInt()!==0) )
self.adjustmentLabel = ko.pureComputed( () =>
(self.adjustmentsInt()>=0)?'Adjustments':'Discounts' )
self.adjustmentsString = ko.pureComputed(() =>
'$'+Math.abs(self.adjustmentsInt()).toFixed(2) )
self.total = ko.computed( () =>
self.subtotal() + self.shippingFeeInt() + self.tax() + self.adjustmentsInt() )
self.totalString = ko.pureComputed( () =>
'$' + self.total().toFixed(2) )
self.trackingNumber = ko.observable('')
self.carrier = ko.observable('')
self.isCreating = ko.observable(false)
self.addBtnText = ko.pureComputed(() =>
self.isCreating()?'Submitting...':'Submit')
self.createInvoice = async () => {
self.isCreating(true)
//TODO: Validate inputs, including API requests
console.log(self.items())
console.log(self.instances())
try {
const res = await fetch(`${API_URL}/admin/invoices/`, {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
user: self.userid(),
note: self.note(),
date: {
ordered: self.date.ordered(),
due: self.date.due(),
paid: self.date.paid(),
shipped: self.date.shipped(),
delivered: self.date.delivered(),
},
items: self.items().map( (i) => ({
item: i.id,
qty: i.qty,
price: Number(i.price),
tax: i.tax,
}) ),
instances: self.instances().map( (i) => ({
instance: i.id,
period: i.period,
price: i.price,
tax: 0,
}) ),
shippingFee: self.shippingFeeInt(),
adjustments: Number(self.adjustments()),
tracking: {
number: self.trackingNumber(),
carrier: self.carrier(),
},
}),
})
if (!res.ok) {
if (res.status===403) logout()
else {
console.error(res)
try {
alert(await res.text())
} catch (err) {
if (err) console.error(err)
alert(`Error response`)
}
}
} else window.location.href = '/admin/invoices/'
} catch (err) {
if (err) console.error(err)
alert('Create request failed')
} finally { self.isCreating(false) }
}
} ko.applyBindings(new Page())
const logout = () => {
window.localStorage.clear()
window.location.href = '/login/'
}
</script>

View File

@ -0,0 +1,128 @@
---
layout: layouts/base.njk
---
<h1>Edit Offering</h1>
<noscript><p>Uh-oh, you don't have javascript. You can't edit this offering without it! </p></noscript>
<a href="/admin/users/">&lt; Back to offering list</a>
<p><input
type="text"
data-bind="textInput:name, event:{keypress:inputKeypress}"
placeholder="Name"
required>
</p>
<p><input
type="text"
data-bind="textInput:description, event:{keypress:inputKeypress}"
placeholder="Description"
required>
</p>
<h2>Default pricing</h2>
<table>
<tbody data-bind="foreach:Object.keys(defaultRates())"><tr>
<td data-bind="text:$data"></td>
<td><input type="number" placeholder="Price" data-bind="textInput:$parent.defaultRates()[$data]"></td>
</tr></tbody>
</table>
<p><button
data-bind="click:updateOffering, disable:isUpdating, text:saveBtnText">
</button></p>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global URLSearchParams fetch ko */
//const API_URL = 'https://api.slvit.us/admin/offerings'
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin/offerings'
function Page() {
let self = this
self.name = ko.observable('')
self.description = ko.observable('')
self.defaultRates = ko.observable({})
self.isUpdating = ko.observable(false)
self.saveBtnText = ko.computed(() =>
self.isUpdating()?'Saving...':'Save')
// Get query params
const urlParams = new URLSearchParams(window.location.search)
const offeringid = urlParams.get('id')
// Submit form on pressing ENTER
// https://stackoverflow.com/a/25055138/3006854
self.inputKeypress = (d,e) => {
if (e.keyCode === 13)
self.updateOffering()
//else {
// TODO: Validate inputs on this textbox (locally, no api requests)
//}
return true
}
self.updateOffering = async () => {
self.isUpdating(true)
//TODO: Validate inputs, including API requests
try {
const res = await fetch(`${API_URL}/${offeringid}/`, {
method: 'PUT',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: self.name(),
description: self.description(),
defaultRates: self.defaultRates(),
}),
})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
}
else if (res.status===404) alert('No offerings found!')
else if (res.status===502) alert('Proxy error; server is down or restarting...')
else if (res.status===500) alert('Server error!')
else alert(`${res.status} error received!`)
} else window.location.href = '/admin/offerings/'
} catch (err) {
if (err) console.error(err)
alert('Update request failed')
} finally { self.isUpdating(false) }
}
// Load offering
;(async () => {
try {
const res = await fetch(`${API_URL}/${offeringid}/`,{credentials:'include'})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
} else if (res.status===404) alert('Offering not found!')
else if (res.status===502) alert('Proxy error; server is down or restarting...')
else if (res.status===500) alert('Server error!')
else alert(`${res.status} error received!`)
return window.location.href = '/admin/offerings/'
} else try {
const offering = await res.json()
console.log(offering)
self.name(offering.name)
self.description(offering.description)
self.defaultRates(offering.defaultRates)
} catch(err) {
console.error('Failed to parse response!')
if(err) console.error(err)
alert(`Failed to load data!`)
}
} catch (err) {
console.error('Failed to send data request!')
if (err) console.error(err)
alert(`Failed to load data! Are you online?`)
}
})()
} ko.applyBindings(new Page())
</script>

View File

@ -0,0 +1,110 @@
---
layout: layouts/base.njk
title: Offerings
---
<h1>{{title}}</h1>
<noscript><p>This dashboard won't work without javascript!</p></noscript>
<div class="flex">
<a href="/admin/">&lt; Back to admin dashboard</a>
<button onclick="window.location.href='new'">Create new offering</button>
</div>
<table>
<thead><tr>
<th>Name</th>
<th>Description</th>
<th>Instances</th>
<th>Actions</th>
</tr></thead>
<tbody data-bind="foreach:offerings"><tr>
<td data-bind="text:name"></td>
<td data-bind="text:desc"></td>
<td data-bind="text:instances.length"></td>
<td>
<button data-bind="click:$parent.editOffering">Edit</button>
<button data-bind="click:$parent.delOffering, disable:$data.isDeleting, text:$data.deleteText">Delete</button>
</td>
</tr></tbody>
</table>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global fetch ko */
//const API_URL = 'https://api.slvit.us/admin'
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin'
function Offering(data) {
this.id = ko.observable(data._id)
this.name = ko.observable(data.name)
this.desc = ko.observable(data.description)
this.instances = ko.observable(data.instances)
this.isDeleting = ko.observable(false)
this.deleteText = ko.computed(() =>
this.isDeleting()?'Deleting...':'Delete')
}
function Page() {
let self = this
self.offerings = ko.observableArray([])
// Delete offering
self.delOffering = async (offering) => {
offering.isDeleting(true)
try {
const res = await fetch(`${API_URL}/offerings/${offering.id()}`, {
method: 'DELETE',
credentials: 'include',
})
if (!res.ok) {
alert(`Request to delete ${offering.name()} failed!`)
console.log(res)
} else {
self.offerings.remove(offering)
}
} catch (err) {
alert(`Failed to send request to delete ${offering.name()}`)
if (err) console.error(err)
} finally {
offering.isDeleting(false)
}
}
// Edit offering
self.editOffering = (offering) => window.location.href = `edit/?id=${offering.id()}`
// Load data
;(async() => {
try {
const res = await fetch(`${API_URL}/offerings/`, {credentials:'include'})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
}
else if (res.status===404) alert('No offerings found!')
else if (res.status===502) alert('Proxy error; server is down or restarting...')
else if (res.status===500) alert('Server error!')
else alert(`${res.status} error received!`)
} else try {
self.offerings( (await res.json())
.map((offering) => new Offering(offering) )
)
} catch(err) {
console.error('Failed to parse response!')
if(err) console.error(err)
alert(`Failed to load data!`)
}
} catch (err) {
console.error('Failed to send data request!')
if (err) console.error(err)
alert(`Failed to load data! Are you online?`)
}
})()
} ko.applyBindings(new Page())
</script>

View File

@ -0,0 +1,103 @@
---
layout: layouts/base.njk
---
<h1>New Offering</h1>
<noscript><p>Uh-oh, you don't have javascript. You can't create an offering without it! </p></noscript>
<a href="/admin/users/">&lt; Back to offering list</a>
<p><input
type="text"
data-bind="textInput:name, event:{keypress:inputKeypress}"
placeholder="Name"
required>
</p>
<p><input
type="text"
data-bind="textInput:description, event:{keypress:inputKeypress}"
placeholder="Description (optional)">
</p>
<h2>Default pricing</h2>
<table>
<tbody data-bind="foreach:Object.keys(defaultRates())"><tr>
<td data-bind="text:$data"></td>
<td><input type="number" placeholder="Price" data-bind="textInput:$parent.defaultRates()[$data]"></td>
</tr></tbody>
</table>
<p><button
data-bind="click:createOffering, disable:isCreating, text:addBtnText">
</button></p>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global fetch ko */
//const API_URL = 'https://api.slvit.us/admin/offerings/'
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin/offerings/'
function Page() {
let self = this
self.name = ko.observable('')
self.description = ko.observable('')
self.isCreating = ko.observable(false)
self.defaultRates = ko.observable({
onetime: 0,
hourly: 0,
daily: 0,
weekly: 0,
monthly: 0,
quarterly: 0,
yearly: 0,
})
self.addBtnText = ko.computed(() =>
self.isCreating()?'Adding...':'Add')
// Submit form on pressing ENTER
// https://stackoverflow.com/a/25055138/3006854
self.inputKeypress = (d,e) => {
if (e.keyCode === 13)
self.createOffering()
//else {
// TODO: Validate inputs on this textbox (locally, no api requests)
//}
return true
}
self.createOffering = async () => {
self.isCreating(true)
//TODO: Validate inputs, including API requests
try {
const res = await fetch(API_URL, {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: self.name(),
description: self.description(),
defaultRates: self.defaultRates(),
}),
})
if (!res.ok) {
if (res.status===403) {
window.localStorage.clear()
window.location.href = '/login/'
} else {
console.error(res)
try {
alert(await res.text())
} catch (err) {
if (err) console.error(err)
alert(`Error response`)
}
}
} else window.location.href = '/admin/offerings/'
} catch (err) {
if (err) console.error(err)
alert('Create request failed')
} finally { self.isCreating(false) }
}
} ko.applyBindings(new Page())
</script>

268
_src/admin/users/edit.njk Normal file
View File

@ -0,0 +1,268 @@
---
layout: layouts/base.njk
---
<h1>Edit User</h1>
<noscript><p>Uh-oh, you don't have javascript. This you can't edit this user without it! </p></noscript>
<a href="/admin/users/">&lt; Back to user list</a>
<p><input
type="text"
data-bind="textInput:username"
placeholder="Username"
required>
</p>
<p><input
type="text"
data-bind="textInput:id"
required disabled>
</p>
<p><input
type="email"
data-bind="textInput:email"
placeholder="Email"
required>
</p>
<p><input
type="password"
data-bind="textInput:password"
placeholder="Password (leave blank for unchanged)"
required>
</p>
<p>
<input
type="text"
data-bind="textInput:firstName"
placeholder="First Name"
required>
</p>
<p>
<input
type="text"
data-bind="textInput:lastName"
placeholder="Last Name"
required>
</p>
<p>
<input
name="is-admin" type="checkbox"
data-bind="checked:isAdmin">
<label for="is-admin">Administrator</label>
</p>
<p><button
data-bind="click:updateUser, disable:isUpdating, text:saveBtnText">
</button></p>
<h2>Instances (<span data-bind="text:instances().length"></span>)</h2>
<p data-bind="visible:instances().length===0">No instances</p>
<table data-bind="visible:instances().length!==0">
<thead><tr>
<th>Name</th>
<th>Rate</th>
<th>Offering</th>
<th>Actions</th>
</tr></thead>
<tbody data-bind="foreach:instances"><tr>
<td data-bind="text:name"></td>
<td>
$<span data-bind="text:rate"></span>
<span data-bind="text:period"></span>
</td>
<td data-bind="text:offering"></td>
<td>
<button data-bind="click:edit">Edit</button>
<button data-bind="click:del.bind($parent.instances.remove), disable:isDeleting, text:deleteText">Delete</button>
</td>
</tr></tbody>
</table>
<button data-bind="click:newInstance">Create new instance</button>
<!--<h2>Orders (<span data-bind="text:orders().length"></span>)</h2>-->
<!--<p data-bind="visible:orders().length===0">No orders</p>-->
<!--<button data-bind="click:newOrder">Create new order</button>-->
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global URLSearchParams fetch ko */
//const API_URL = 'https://api.slvit.us/admin'
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin'
function Instance(data) {
this.id = ko.observable(data._id)
this.name = ko.observable(data.name)
this.note = ko.observable(data.note)
this.rate = ko.observable(data.rate)
this.period = ko.observable(data.period)
this.offering = ko.observable(data.offering.name)
this.isDeleting = ko.observable(false)
this.deleteText = ko.computed(() =>
this.isDeleting()?'Deleting...':'Delete')
this.edit = function() {
window.location.href =
`/admin/instances/edit/?id=${this.id()}`
}
this.del = async function(rm) {
this.isDeleting(true)
if (confirm(`Are you sure you want to delete ${this.name()}?`)) {
try {
const res = await fetch(`${API_URL}/instances/${this.id()}`, {
method: 'DELETE',
credentials: 'include',
})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
}
else if (res.status===404)
alert('Instance not found!')
else if (res.status===502)
alert('Proxy error; server is down or restarting...')
else if (res.status===500)
alert('Server error!')
else alert(`${res.status} error received!`)
} else rm(this)
} catch (err) {
if (err) console.error(err)
alert('Failed to send DELETE request!')
} finally { this.isDeleting(false) }
} else this.isDeleting(false)
}
}
// function Order() {
// TODO
// }
function Page() {
const urlParams = new URLSearchParams(window.location.search)
let self = this
self.username = ko.observable('')
self.password = ko.observable('')
self.email = ko.observable('')
self.firstName = ko.observable('')
self.lastName = ko.observable('')
self.isAdmin = ko.observable(false)
self.instances = ko.observableArray([])
self.orders = ko.observableArray([])
self.isUpdating = ko.observable(false)
self.saveBtnText = ko.computed(() =>
self.isUpdating()?'Saving...':'Save')
self.id = ko.observable(urlParams.get('id'))
self.updateUser = async () => {
self.isUpdating(true)
//TODO: Validate inputs, including API requests
try {
const res = await fetch(`${API_URL}/admin/users/${self.id()}/`, {
method: 'PUT',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: self.username(),
email: self.email(),
name: {
first: self.firstName(),
last: self.lastName(),
},
isAdmin: self.isAdmin(),
password: self.password(),
}),
})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
}
else if (res.status===404)
alert('No users found!')
else if (res.status===502)
alert('Proxy error; server is down or restarting...')
else if (res.status===500)
alert('Server error!')
else alert(`${res.status} error received!`)
} else window.location.href = '/admin/users/'
} catch (err) {
if (err) console.error(err)
alert('Update request failed')
} finally { self.isUpdating(false) }
}
self.newInstance = () => {
window.location.href = `/admin/instances/new/?id=${self.id()}`
}
// Delete instance
self.delInstance = async (instance) => {
instance.isDeleting(true)
try {
const res = await fetch(`${API_URL}/instances/${instance.id()}`, {
method: 'DELETE',
credentials: 'include',
})
if (!res.ok) {
alert(`Request to delete ${instance.name()} failed!`)
console.log(res)
} else {
self.instances.remove(instance)
}
} catch (err) {
alert(`Failed to send request to delete ${instance.name()}`)
if (err) console.error(err)
} finally {
instance.isDeleting(false)
}
}
// Edit instance
// self.editInstance = () => {
// console.log(`editInstance called for ${this}`)
// window.location.href = `edit/?id=${this.id()}`
// }
// self.newOrder = () => {
// window.location.href = `/admin/orders/new/user/?id=${userid}`
// }
// Load user
;(async () => {
try {
const res = await fetch(`${API_URL}/users/${self.id()}/`,{credentials:'include'})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
} else if (res.status===404) alert('User not found!')
else if (res.status===502) alert('Proxy error; server is down or restarting...')
else if (res.status===500) alert('Server error!')
else alert(`${res.status} error received!`)
return window.location.href = '/admin/users/'
} else try {
const user = await res.json()
console.log(user)
self.username(user.username)
self.email(user.email)
self.firstName(user.name.first)
self.lastName(user.name.last)
self.isAdmin(user.isAdmin)
self.instances( user.instances
.map ((instance) => new Instance(instance) )
)
self.orders(user.orders)
} catch(err) {
console.error('Failed to parse response!')
if(err) console.error(err)
alert(`Failed to load data!`)
}
} catch (err) {
console.error('Failed to send data request!')
if (err) console.error(err)
alert(`Failed to load data! Are you online?`)
}
})()
} ko.applyBindings(new Page())
</script>

120
_src/admin/users/index.njk Normal file
View File

@ -0,0 +1,120 @@
---
layout: layouts/base.njk
title: Users
---
<h1>{{title}}</h1>
<noscript><p>This dashboard won't work without javascript!</p></noscript>
<div class="flex">
<a href="/admin/">&lt; Back to admin dashboard</a>
<button onclick="window.location.href='new'">Create new user</button>
</div>
<table>
<thead><tr>
<th>Usename</th>
<th>Created</th>
<th>Last login</th>
<th>Admin</th>
<th>Actions</th>
</tr></thead>
<tbody data-bind="foreach:users"><tr>
<td data-bind="text:username"></td>
<td data-bind="text:createdLocale"></td>
<td data-bind="text:lastLoginLocale"></td>
<td data-bind="text:isAdmin"></td>
<td>
<button data-bind="click:$parent.editUser">Edit</button>
<button data-bind="click:$parent.delUser, disable:$data.isDeleting, text:$data.deleteText">Delete</button>
</td>
</tr></tbody>
</table>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global fetch ko */
//const API_URL = 'https://api.slvit.us/admin'
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin'
function User(data) {
this.id = ko.observable(data._id)
this.username = ko.observable(data.username)
this.created = ko.observable(data.created)
this.lastLogin = ko.observable(data.lastLogin)
this.isAdmin = ko.observable(data.isAdmin)
this.isDeleting = ko.observable(false)
this.createdLocale = ko.computed(() =>
new Date(data.created).toLocaleDateString())
this.lastLoginLocale = ko.computed(() => (data.lastLogin)
? new Date(data.lastLogin).toLocaleDateString()
: 'Never'
)
this.deleteText = ko.computed(() =>
this.isDeleting()?'Deleting...':'Delete')
}
function Page() {
let self = this
self.users = ko.observableArray([])
// Delete user
self.delUser = async (user) => {
user.isDeleting(true)
try {
const res = await fetch(`${API_URL}/users/${user.id()}`, {
method: 'DELETE',
credentials: 'include',
})
if (!res.ok) {
alert(`Request to delete ${user.username()} failed!`)
console.log(res)
} else {
self.users.remove(user)
}
} catch (err) {
alert(`Failed to send request to delete ${user.username()}`)
if (err) console.error(err)
} finally {
user.isDeleting(false)
}
}
// Edit user
self.editUser = (user) => window.location.href = `edit/?id=${user.id()}`
// Load data
;(async() => {
try {
const res = await fetch(`${API_URL}/users/`,{credentials:'include'})
if (!res.ok) {
console.error(res)
if (res.status===403) {
window.localStorage.clear()
return window.location.href = '/login/'
}
else if (res.status===404) alert('No users found!')
else if (res.status===502) alert('Proxy error; server is down or restarting...')
else if (res.status===500) alert('Server error!')
else alert(`${res.status} error received!`)
} else try {
self.users( (await res.json())
.map((user) => new User(user) )
.sort( (a,b) => a.lastLogin - b.lastLogin )
)
} catch(err) {
console.error('Failed to parse response!')
if(err) console.error(err)
alert(`Failed to load data!`)
}
} catch (err) {
console.error('Failed to send data request!')
if (err) console.error(err)
alert(`Failed to load data! Are you online?`)
}
})()
} ko.applyBindings(new Page())
</script>

122
_src/admin/users/new.njk Normal file
View File

@ -0,0 +1,122 @@
---
layout: layouts/base.njk
---
<h1>New User</h1>
<noscript><p>Uh-oh, you don't have javascript. This you can't create a user without it! </p></noscript>
<a href="/admin/users/">&lt; Back to user list</a>
<p><input
type="text"
data-bind="textInput:username, event:{keypress:inputKeypress}"
placeholder="Username"
required>
</p>
<p><input
type="email"
data-bind="textInput:email, event:{keypress:inputKeypress}"
placeholder="Email"
required>
</p>
<p><input
type="password"
data-bind="textInput:password, event:{keypress:inputKeypress}"
placeholder="Password"
required>
</p>
<p>
<input
type="text"
data-bind="textInput:firstName, event:{keypress:inputKeypress}"
placeholder="First Name"
required>
</p>
<p>
<input
type="text"
data-bind="textInput:lastName, event:{keypress:inputKeypress}"
placeholder="Last Name"
required>
</p>
<p>
<input
name="is-admin" type="checkbox"
data-bind="checked:isAdmin, event:{keypress:inputKeypress}">
<label for="is-admin">Administrator</label>
</p>
<p><button
data-bind="click:createUser, disable:isCreating, text:addBtnText">
</button></p>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global fetch ko */
//const API_URL = 'https://api.slvit.us/admin/users/'
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin/users/'
function Page() {
let self = this
self.username = ko.observable('')
self.password = ko.observable('')
self.email = ko.observable('')
self.firstName = ko.observable('')
self.lastName = ko.observable('')
self.isAdmin = ko.observable(false)
self.isCreating = ko.observable(false)
self.addBtnText = ko.computed(() =>
self.isCreating()?'Adding...':'Add')
// Submit form on pressing ENTER
// https://stackoverflow.com/a/25055138/3006854
self.inputKeypress = (d,e) => {
if (e.keyCode === 13)
self.createUser()
//else {
// TODO: Validate inputs on this textbox (locally, no api requests)
//}
return true
}
self.createUser = async () => {
self.isCreating(true)
//TODO: Validate inputs, including API requests
try {
const res = await fetch(API_URL, {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: self.username(),
email: self.email(),
name: {
first: self.firstName(),
last: self.lastName(),
},
isAdmin: self.isAdmin(),
password: self.password(),
}),
})
if (!res.ok) {
if (res.status===403) {
window.localStorage.clear()
window.location.href = '/login'
} else {
console.error(res)
try {
alert(await res.text())
} catch (err) {
if (err) console.error(err)
alert(`Error response`)
}
}
} else window.location.href = '/admin/users/'
} catch (err) {
if (err) console.error(err)
alert('Create request failed')
} finally { self.isCreating(false) }
}
} ko.applyBindings(new Page())
</script>

View File

@ -33,6 +33,7 @@
* {
box-sizing: border-box;
}
scrollbar-color: var(--darkgray) #999;
::-webkit-scrollbar {
width: 5vw;
max-width: 20px;
@ -111,10 +112,10 @@ input, textarea, button {
font-family: inherit;
border-style: solid;
border-width: 1px;
width: 100%;
border-radius: calc(.2vw + .2vh);
}
textarea, input {
textarea, input:not([type='checkbox']) {
width: 100%;
background-color: rgba(0,0,0,.07);
box-shadow: inset calc(.2vw + .2vh) calc(.2vw + .2vh) calc(.5vw + .5vh) rgba(0,0,0,.5);
padding: calc(.2vw + .2vh);
@ -225,7 +226,7 @@ table th {
}
hr {
margin: 3rem auto;
margin: 2rem auto;
}
pre {
line-height: 1.375;
@ -244,8 +245,13 @@ pre {
padding: 1em;
margin: .5em 0;
background-color: #000;
}
table, pre {
overflow-x: scroll;
}
table {
display: block;
}
code {
background-color: #000;
word-break: break-all;

View File

@ -1,65 +0,0 @@
/* global openpgp fetch hcaptcha ko */
const API_URL = "https://mailapi.slvit.us/"
function Form() {
let self = this
self.name = ko.observable('')
self.email = ko.observable('')
self.subj = ko.observable('')
self.body = ko.observable('')
self.isSending = ko.observable(false)
self.sendBtnText = ko.computed(() => self.isSending()?'Sending...':'Send')
self.sendMsg = async () => {
self.isSending(true)
let capRes; try {
capRes = await hcaptcha.execute(
null, {async:true}
)
}
catch (err) {
alert(`Failed to submit hCaptcha. Try again later.`)
console.error('Failed to run hCaptcha')
if (err) console.error(err)
}
let res; try {
res = await fetch(API_URL, {
method: 'POST',
cache: 'no-cache',
headers: {'content-type': 'application/json'},
body: JSON.stringify({
token: capRes.response,
name: self.name(),
subj: self.subj(),
email: self.email(),
msg: self.body(),
// msg: await openpgp.encrypt({
// message: await openpgp.createMessage(
// { text: `\n${self.body()}\n\n` }
// ),
// encryptionKeys: await openpgp.readKey({
// armoredKey: `-----BEGIN PGP PUBLIC KEY BLOCK-----
// =Wvbt
// -----END PGP PUBLIC KEY BLOCK-----`,
// }),
// }),
}),
})
} catch (err) {
console.error(err)
alert(`Failed to connect to the network. Are you online?`)
} finally { self.isSending(false) }
if (res.status===200) {
alert('Your message was sent successfully.')
self.name(''); self.email(''); self.subj(''); self.body('')
} else if (res.status===403)
alert(`hCaptcha failed! Please try again.`)
else if (res.status===500)
alert(`Backend failed! Please try again. If the problem persists, please email hostmaster@[this domain].`)
else alert(`Unknown error! Please try again. If the problem persists, please email hostmaster@[this domain].`)
}
}
ko.applyBindings(new Form())

97
_src/dashboard/index.njk Normal file
View File

@ -0,0 +1,97 @@
---
layout: layouts/base.njk
---
<h1>Dashboard</h1>
<noscript><p>This dashboard won't work without javascript!</p></noscript>
<p>Hello <span data-bind="text:name">user</span>.</p>
<h2>Services (<span data-bind="text:services().length"></span>)</h2>
<p data-bind="visible:services().length===0">No services</p>
<table data-bind="visible:services().length!==0">
<thead><tr>
<th>Name</th>
<th>Type</th>
<th>Rate</th>
<th>Next invoice</th>
</tr></thead>
<tbody data-bind="foreach:services"><tr>
<td data-bind="text:name"></td>
<td data-bind="text:offering.name"></td>
<td>
$<span data-bind="text:rate"></span>
<span data-bind="text:period"></span>
</td>
<td></td>
</tr></tbody>
</table>
<h2>Billing (<span data-bind="text:invoices().length"></span>)</h2>
<p data-bind="visible:invoices().length===0">No invoices</p>
<table data-bind="visible:invoices().length!==0">
<thead><tr>
<th>Number</th>
<th>Total</th>
<th>Due</th>
<th>Paid</th>
</tr></thead>
<tbody data-bind="foreach:invoices"><tr>
<td data-bind="text:_id"></td>
<!--<td data-bind="text:total"></td>-->
<!--<td data-bind="text:due"></td>-->
<!--<td data-bind="text:paid"></td>-->
</tr></tbody>
</table>
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
<script> /* global ko fetch */
//const API_URL = 'https://api.slvit.us'
const API_URL = 'https://dev-api.ksx.ki9.slvit'
function Dashboard() {
let self = this
self.services = ko.observableArray([])
self.invoices = ko.observableArray([])
self.name = ko.observable('user')
// Load dashboard
;(async () => {
try {
const res = await fetch(`${API_URL}/dashboard`, {
credentials: 'include',
})
if (!res.ok) {
if (res.status===403) {
window.localStorage.clear()
window.location.href = '/login/'
} else alert(`Error response!`)
} else {
try {
const parsedRes = await res.json()
console.log(parsedRes)
if (typeof parsedRes.instances==='object')
self.services(parsedRes.instances)
if (typeof parsedRes.invoices==='object')
self.invoices(parsedRes.invoices)
if (typeof parsedRes.user.name.first==='string')
self.name(parsedRes.user.name.first)
else if (typeof parsedRes.user.username==='string')
self.name(parsedRes.user.username)
} catch (err) {
if (err) console.error(err)
alert('Failed to parse JSON!')
}
}
} catch (err) {
if (err) console.error(err)
alert('Failed to send API request!')
}
})()
}
// Check login
ko.applyBindings(new Dashboard())
</script>

10
_src/login.md Normal file
View File

@ -0,0 +1,10 @@
---
layout: layouts/base.njk
title: Login
---
# {{title}}
Use this form to log in.
{% include 'login-form.njk' %}

33
_src/logout.njk Normal file
View File

@ -0,0 +1,33 @@
---
layout: layouts/base.njk
title: Logout
---
<h1>{{title}}</h1>
<p>Logging out...</p>
<noscript><p>You can't log out without javascript!</p></noscript>
<script>/* global fetch */
//const API_URL = 'https://api.slvit.us'
const API_URL = 'https://dev-api.ksx.ki9.slvit'
;(async () => {
try {
const res = await fetch(`${API_URL}/logout`, {
method: 'POST',
credentials: 'include',
})
if (res.ok) {
window.localStorage.clear()
window.location.href = '/'
}
else {
console.error(res)
alert('Failed to log out!')
}
}
catch (err) {
console.error(err)
alert('Failed to log out!')
}
})()
</script>

6
_src/speedtest.njk Normal file
View File

@ -0,0 +1,6 @@
---
layout: layouts/base.njk
title: Speedtest
---
<iframe id="speedtest-frame" src="https://speedtest.slvit.us/" style="border:none;width:100%;min-height:700px;height:calc(70vh + 10vw)"></iframe>

10
_src/websearch.md Normal file
View File

@ -0,0 +1,10 @@
---
layout: layouts/base.njk
title: Web search
---
# {{title}}
Use this page to anonymously search the web.
{% include 'websearch.njk' %}