summaryrefslogtreecommitdiff
path: root/public/js
diff options
context:
space:
mode:
Diffstat (limited to 'public/js')
-rw-r--r--public/js/api.js51
-rw-r--r--public/js/components.js12
-rw-r--r--public/js/domain.js95
-rw-r--r--public/js/home.js91
-rw-r--r--public/js/login.js44
-rw-r--r--public/js/main.js136
6 files changed, 429 insertions, 0 deletions
diff --git a/public/js/api.js b/public/js/api.js
new file mode 100644
index 0000000..7a29d65
--- /dev/null
+++ b/public/js/api.js
@@ -0,0 +1,51 @@
+const endpoint = '/api'
+
+const request = async (url, method, body) => {
+
+ let response;
+
+ if (method == 'GET') {
+ response = await fetch(endpoint + url, {
+ method,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+ } else {
+ response = await fetch(endpoint + url, {
+ method,
+ body: JSON.stringify(body),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+ }
+
+ if (response.status == 401) {
+ location.href = '/login'
+ }
+ const contentType = response.headers.get("content-type");
+ if (contentType && contentType.indexOf("application/json") !== -1) {
+ const json = await response.json()
+ return { status: response.status, msg: json.msg, json }
+ } else {
+ const msg = await response.text();
+ return { status: response.status, msg }
+ }
+}
+
+export const login = async (user, pass) => {
+ return await request('/login', 'POST', {user, pass})
+}
+
+export const domains = async () => {
+ return await request('/domains', 'GET')
+}
+
+export const del_domain = async (domain) => {
+ return await request('/domains', 'DELETE', {domain})
+}
+
+export const records = async (domain) => {
+ return await request(`/records?domain=${domain}`, 'GET')
+} \ No newline at end of file
diff --git a/public/js/components.js b/public/js/components.js
new file mode 100644
index 0000000..00def5f
--- /dev/null
+++ b/public/js/components.js
@@ -0,0 +1,12 @@
+import { div, parse, span } from './main.js';
+
+export function header(title) {
+ return div({id: 'header'},
+ span({id: 'logo', class: 'accent'},
+ parse("Wrapper")
+ ),
+ span({id: 'title'},
+ parse(title)
+ ),
+ )
+} \ No newline at end of file
diff --git a/public/js/domain.js b/public/js/domain.js
new file mode 100644
index 0000000..36af422
--- /dev/null
+++ b/public/js/domain.js
@@ -0,0 +1,95 @@
+import { del_domain, domains, records } from './api.js'
+import { header } from './components.js'
+import { body, parse, div, input, button, span, is_domain } from './main.js';
+
+function render(domain, records) {
+
+ let divs = []
+ for (const record of records) {
+ divs.push(gen_record(record))
+ }
+
+ document.body.replaceWith(
+ body({},
+ header(domain),
+ div({id: 'buttons'},
+ button({onclick: (event) => {
+ location.href = '/home'
+ }}, parse("Home")),
+ button({}, parse("New Record")),
+ ),
+ ...divs
+ )
+ )
+}
+
+function gen_record(record) {
+ let domain = record.domain
+ let prefix = record.prefix
+
+ if (prefix.length > 0) {
+ prefix = prefix + '.'
+ }
+
+ let type = Object.keys(record.record)[0]
+ let data = record.record[type]
+
+ let divs = []
+ for (const key in data) {
+ let disp_key;
+ if (key == 'ttl') {
+ disp_key = 'TTL'
+ } else {
+ disp_key = upper(key)
+ }
+ divs.push(
+ div({class: 'poperty'},
+ div({class: 'key'}, parse(disp_key)),
+ div({class: 'value'}, parse(data[key])),
+ )
+ )
+ }
+
+ return div({class: 'record'},
+ div({class: 'header'},
+ span({class: 'type'}, parse(type)),
+ span({class: 'prefix'}, parse(prefix)),
+ span({class: 'domain'}, parse(domain)),
+ button({}, parse("Edit")),
+ button({class: 'delete'}, parse("Delete"))
+ ),
+ div({class: 'properties'},
+ ...divs
+ )
+ )
+}
+
+function upper(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+async function init() {
+
+ const params = new Proxy(new URLSearchParams(window.location.search), {
+ get: (searchParams, prop) => searchParams.get(prop),
+ });
+
+ let domain = params.domain;
+
+ if (!is_domain(domain)) {
+ location.href = '/home'
+ return
+ }
+
+ let res = await records(domain);
+
+ if (res.status !== 200) {
+ alert(res.msg)
+ return
+ }
+
+ render(domain, res.json)
+
+}
+
+init() \ No newline at end of file
diff --git a/public/js/home.js b/public/js/home.js
new file mode 100644
index 0000000..f615632
--- /dev/null
+++ b/public/js/home.js
@@ -0,0 +1,91 @@
+import { del_domain, domains } from './api.js'
+import { header } from './components.js'
+import { body, parse, div, input, button, span, is_domain } from './main.js';
+
+function render(domains) {
+ document.body.replaceWith(
+ body({},
+ header('domains'),
+ div({id: 'new'},
+ input({
+ type: 'text',
+ name: 'domain',
+ id: 'domain',
+ placeholder: 'Type domain name to create new records',
+ autocomplete: "off",
+ }),
+ button({onclick: () => {
+ let domain = document.getElementById('domain').value
+
+ if (!is_domain(domain)) {
+ alert("Invalid domain")
+ return
+ }
+
+ location.href = '/domain?domain='+domain
+ }},
+ parse("Create")
+ )
+ ),
+ ...domain(domains)
+ )
+ )
+}
+
+function domain(domains) {
+ let divs = []
+ for (const domain of domains) {
+ divs.push(
+ div({class: 'domain'},
+ div({class: 'block'},
+ parse(domain)
+ ),
+ button({class: 'edit', onclick: (event) => {
+ console.log(event.target.parentElement)
+ let domain = event
+ .target
+ .parentElement
+ .getElementsByClassName('block')[0]
+ .innerText
+
+ if (!is_domain(domain)) {
+ alert("Invalid domain")
+ return
+ }
+
+ location.href = '/domain?domain='+domain
+ }},
+ parse("Edit")
+ ),
+ button({class: 'delete', onclick: async () => {
+ let res = await del_domain(domain)
+
+ if (res.status != 204) {
+ alert(res.msg)
+ return
+ }
+
+ location.reload()
+ }},
+ parse("Delete")
+ )
+ )
+ )
+ }
+ return divs
+}
+
+async function init() {
+
+ let res = await domains();
+
+ if (res.status !== 200) {
+ alert(res.msg)
+ return
+ }
+
+ render(res.json)
+
+}
+
+init() \ No newline at end of file
diff --git a/public/js/login.js b/public/js/login.js
new file mode 100644
index 0000000..3bd64ad
--- /dev/null
+++ b/public/js/login.js
@@ -0,0 +1,44 @@
+import { body, div, form, input, p, parse, span} from './main.js'
+import { login } from './api.js'
+
+function render() {
+ document.body.replaceWith(
+ body({},
+ div({id: 'login', class: 'fill'},
+ span({id: 'logo'},
+ span({class: 'accent'}, parse('Wrapper'))
+ ),
+ form({autocomplete: "off"},
+ input({
+ type: 'text',
+ name: 'user',
+ id: 'user',
+ placeholder: 'Username',
+ autofocus: 1
+ }),
+ input({
+ type: 'password',
+ name: 'pass',
+ id: 'pass',
+ placeholder: 'Password',
+ onkeydown: async (event) => {
+ if (event.key == 'Enter') {
+ event.preventDefault()
+ let user = document.getElementById('user').value
+ let pass = document.getElementById('pass').value
+
+ let res = await login(user, pass)
+
+ if (res.status === 200) {
+ location.href = '/home'
+ }
+ }
+ }
+ })
+ )
+ )
+ )
+ )
+}
+
+render()
diff --git a/public/js/main.js b/public/js/main.js
new file mode 100644
index 0000000..615b3d6
--- /dev/null
+++ b/public/js/main.js
@@ -0,0 +1,136 @@
+function createElement(name, attrs, ...children) {
+ const el = document.createElement(name);
+
+ for (const attr in attrs) {
+ if(attr.startsWith("on")) {
+ el[attr] = attrs[attr];
+ } else {
+ el.setAttribute(attr, attrs[attr])
+ }
+ }
+
+ for (const child of children) {
+ if (child == null) {
+ continue
+ }
+ el.appendChild(child)
+ }
+
+ return el
+}
+
+export function createElementNS(name, attrs, ...children) {
+ var svgns = "http://www.w3.org/2000/svg";
+ var el = document.createElementNS(svgns, name);
+
+ for (const attr in attrs) {
+ if(attr.startsWith("on")) {
+ el[attr] = attrs[attr];
+ } else {
+ el.setAttribute(attr, attrs[attr])
+ }
+ }
+
+ for (const child of children) {
+ if (child == null) {
+ continue
+ }
+ el.appendChild(child)
+ }
+
+ return el
+}
+
+export function p(attrs, ...children) {
+ return createElement("p", attrs, ...children)
+}
+
+export function span(attrs, ...children) {
+ return createElement("span", attrs, ...children)
+}
+
+export function div(attrs, ...children) {
+ return createElement("div", attrs, ...children)
+}
+
+export function a(attrs, ...children) {
+ return createElement("a", attrs, ...children)
+}
+
+export function i(attrs, ...children) {
+ return createElement("i", attrs, ...children)
+}
+
+export function form(attrs, ...children) {
+ return createElement("form", attrs, ...children)
+}
+
+export function img(alt, attrs, ...children) {
+ attrs['onerror'] = (event) => event.target.remove()
+ attrs['alt'] = alt
+ return createElement("img", attrs, ...children)
+}
+
+export function input(attrs, ...children) {
+ return createElement("input", attrs, ...children)
+}
+
+export function button(attrs, ...children) {
+ return createElement("button", attrs, ...children)
+}
+
+export function path(attrs, ...children) {
+ return createElementNS("path", attrs, ...children)
+}
+
+export function svg(attrs, ...children) {
+ return createElementNS("svg", attrs, ...children)
+}
+
+export function body(attrs, ...children) {
+ return createElement("body", attrs, ...children)
+}
+
+export function textarea(attrs, ...children) {
+ return createElement("textarea", attrs, ...children)
+}
+
+export function parse(input) {
+ const pattern = /^[ a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]*$/;
+
+ input = input + '';
+
+ if (!pattern.test(input)) {
+ return null;
+ }
+
+ const sanitized = input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
+ return document.createRange().createContextualFragment(sanitized);
+}
+
+export function is_domain(domain) {
+ domain = domain.toLowerCase()
+
+ const pattern = /^[a-z0-9_\-.]*$/;
+ if (!pattern.test(domain)) {
+ return false
+ }
+
+ let parts = domain.split('.').reverse()
+ for (const part of parts) {
+ if (part.length < 1) {
+ return false
+ }
+ }
+
+ if (parts.length < 2 || parts[0].length < 2) {
+ return false
+ }
+
+ const tld_pattern = /^[a-z]*$/;
+ if (!tld_pattern.test(parts[0])) {
+ return false
+ }
+
+ return true
+} \ No newline at end of file